Finished the coding part of the call trace functionality.

Thu, 01 Nov 2012 18:29:58 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 01 Nov 2012 18:29:58 +0100
changeset 2171
c7dd548d67d8
parent 2170
f4e0f6133ace
child 2172
6d8eb5736925

Finished the coding part of the call trace functionality.

DebugClients/Python/DebugBase.py file | annotate | diff | comparison | revisions
DebugClients/Python/DebugClientBase.py file | annotate | diff | comparison | revisions
DebugClients/Python/DebugProtocol.py file | annotate | diff | comparison | revisions
Debugger/CallTraceViewer.py file | annotate | diff | comparison | revisions
Debugger/CallTraceViewer.ui file | annotate | diff | comparison | revisions
Debugger/DebugUI.py file | annotate | diff | comparison | revisions
Debugger/DebugViewer.py file | annotate | diff | comparison | revisions
Debugger/DebuggerInterfaceNone.py file | annotate | diff | comparison | revisions
Debugger/DebuggerInterfacePython.py file | annotate | diff | comparison | revisions
Debugger/DebuggerInterfaceRuby.py file | annotate | diff | comparison | revisions
Project/Project.py file | annotate | diff | comparison | revisions
--- a/DebugClients/Python/DebugBase.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/DebugClients/Python/DebugBase.py	Thu Nov 01 18:29:58 2012 +0100
@@ -15,7 +15,7 @@
 import inspect
 
 from DebugProtocol import ResponseClearWatch, ResponseClearBreak, ResponseLine, \
-    ResponseSyntax, ResponseException
+    ResponseSyntax, ResponseException, CallTrace
 
 gRecursionLimit = 64
 
@@ -154,13 +154,39 @@
         if event == 'return':
             self.cFrame = frame.f_back
             self.__recursionDepth -= 1
+            self.__sendCallTrace(event, frame, self.cFrame)
         elif event == 'call':
+            self.__sendCallTrace(event, self.cFrame, frame)
             self.cFrame = frame
             self.__recursionDepth += 1
             if self.__recursionDepth > gRecursionLimit:
                 raise RuntimeError('maximum recursion depth exceeded\n'
                     '(offending frame is two down the stack)')
     
+    def __sendCallTrace(self, event, fromFrame, toFrame):
+        """
+        Private method to send a call/return trace.
+        
+        @param event trace event (string)
+        @param fromFrame originating frame (frame)
+        @param toFrame destination frame (frame)
+        """
+        if self._dbgClient.callTraceEnabled:
+            if not self.__skip_it(fromFrame) and not self.__skip_it(toFrame):
+                if event in ["call", "return"]:
+                    fr = fromFrame
+                    fromStr = "%s:%s:%s" % (
+                        self._dbgClient.absPath(self.fix_frame_filename(fr)),
+                        fr.f_lineno,
+                        fr.f_code.co_name)
+                    fr = toFrame
+                    toStr = "%s:%s:%s" % (
+                        self._dbgClient.absPath(self.fix_frame_filename(fr)),
+                        fr.f_lineno,
+                        fr.f_code.co_name)
+                    self._dbgClient.write("%s%s@@%s@@%s\n" % (
+                        CallTrace, event[0], fromStr, toStr))
+    
     def trace_dispatch(self, frame, event, arg):
         """
         Reimplemented from bdb.py to do some special things.
@@ -700,6 +726,9 @@
         @param frame the frame object
         @return flag indicating whether the debugger should skip this frame
         """
+        if frame is None:
+            return 1
+        
         fn = self.fix_frame_filename(frame)
 
         # Eliminate things like <string> and <stdin>.
--- a/DebugClients/Python/DebugClientBase.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/DebugClients/Python/DebugClientBase.py	Thu Nov 01 18:29:58 2012 +0100
@@ -225,6 +225,9 @@
         self.errorstream = None
         self.pollingDisabled = False
         
+        self.callTraceEnabled = False
+        self.__newCallTraceEnabled = False
+        
         self.skipdirs = sys.path[:]
         
         self.variant = 'You should not see this'
@@ -456,6 +459,17 @@
                 self.pendingResponse = DebugProtocol.ResponseOK
                 return
 
+            if cmd == DebugProtocol.RequestCallTrace:
+                if arg.strip().lower() == "on":
+                    callTraceEnabled = True
+                else:
+                    callTraceEnabled = False
+                if self.debugging:
+                    self.callTraceEnabled = callTraceEnabled
+                else:
+                    self.__newCallTraceEnabled = callTraceEnabled   # remember for later
+                return
+            
             if cmd == DebugProtocol.RequestEnv:
                 env = eval(arg)
                 for key, value in env.items():
@@ -506,6 +520,7 @@
                 # IOErrors, if self.running is passed as a normal str.
                 self.debugMod.__dict__['__file__'] = self.running
                 sys.modules['__main__'] = self.debugMod
+                self.callTraceEnabled = self.__newCallTraceEnabled
                 res = self.mainThread.run('execfile(' + repr(self.running) + ')',
                                           self.debugMod.__dict__)
                 self.progTerminated(res)
--- a/DebugClients/Python/DebugProtocol.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/DebugClients/Python/DebugProtocol.py	Thu Nov 01 18:29:58 2012 +0100
@@ -77,6 +77,9 @@
 
 PassiveStartup = '>PassiveStartup<'
 
+RequestCallTrace = '>CallTrace<'
+CallTrace = '>CallTrace<'
+
 EOT = '>EOT<\n'
 
 #
--- a/Debugger/CallTraceViewer.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/Debugger/CallTraceViewer.py	Thu Nov 01 18:29:58 2012 +0100
@@ -7,13 +7,17 @@
 Module implementing the Call Trace viewer widget.
 """
 
-from PyQt4.QtCore import pyqtSlot, pyqtSignal
+from PyQt4.QtCore import pyqtSlot, pyqtSignal, Qt, QRegExp, QFileInfo
 from PyQt4.QtGui import QWidget,  QTreeWidgetItem
 
+from E5Gui.E5Application import e5App
+from E5Gui import E5FileDialog, E5MessageBox
+
 from .Ui_CallTraceViewer import Ui_CallTraceViewer
 
 import UI.PixmapCache
 import Preferences
+import Utilities
 
 
 class CallTraceViewer(QWidget, Ui_CallTraceViewer):
@@ -48,36 +52,45 @@
         self.__callStack = []
         
         self.__entryFormat = "{0}:{1} ({2})"
+        self.__entryRe = QRegExp(r"""(.+):(\d+)\s\((.*)\)""")
+        
+        self.__projectMode = False
+        self.__project = None
         
         self.__callTraceEnabled = Preferences.toBool(
             Preferences.Prefs.settings.value("CallTrace/Enabled", False))
-        
         if self.__callTraceEnabled:
+            self.startTraceButton.setEnabled(False)
+        else:
             self.stopTraceButton.setEnabled(False)
-        else:
-            self.startTraceButton.setEnabled(False)
         
         self.__dbs.callTraceInfo.connect(self.__addCallTraceInfo)
     
+    def __setCallTraceEnabled(self, enabled):
+        """
+        Private slot to set the call trace enabled status.
+        
+        @param enabled flag indicating the new state (boolean)
+        """
+        self.__dbs.setCallTraceEnabled(enabled)
+        self.stopTraceButton.setEnabled(enabled)
+        self.startTraceButton.setEnabled(not enabled)
+        self.__callTraceEnabled = enabled
+        Preferences.Prefs.settings.setValue("CallTrace/Enabled", enabled)
+    
     @pyqtSlot()
     def on_startTraceButton_clicked(self):
         """
         Private slot to start call tracing.
         """
-        self.__dbs.setCallTraceEnabled(True)
-        self.stopTraceButton.setEnabled(True)
-        self.startTraceButton.setEnabled(False)
-        Preferences.Prefs.settings.setValue("CallTrace/Enabled", True)
+        self.__setCallTraceEnabled(True)
     
     @pyqtSlot()
     def on_stopTraceButton_clicked(self):
         """
         Private slot to start call tracing.
         """
-        self.__dbs.setCallTraceEnabled(False)
-        self.stopTraceButton.setEnabled(False)
-        self.startTraceButton.setEnabled(True)
-        Preferences.Prefs.settings.setValue("CallTrace/Enabled", False)
+        self.__setCallTraceEnabled(False)
     
     @pyqtSlot()
     def on_resizeButton_clicked(self):
@@ -97,18 +110,72 @@
     @pyqtSlot()
     def on_saveButton_clicked(self):
         """
-        Slot documentation goes here.
+        Private slot to save the call trace info to a file.
         """
-        # TODO: not implemented yet
-        raise NotImplementedError
+        if self.callTrace.topLevelItemCount() > 0:
+            fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
+                self,
+                self.trUtf8("Save Call Trace Info"),
+                "",
+                self.trUtf8("Text Files (*.txt);;All Files (*)"),
+                None,
+                E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
+            if fname:
+                ext = QFileInfo(fname).suffix()
+                if not ext:
+                    ex = selectedFilter.split("(*")[1].split(")")[0]
+                    if ex:
+                        fname += ex
+                if QFileInfo(fname).exists():
+                    res = E5MessageBox.yesNo(self,
+                        self.trUtf8("Save Call Trace Info"),
+                        self.trUtf8("<p>The file <b>{0}</b> already exists."
+                                    " Overwrite it?</p>").format(fname),
+                        icon=E5MessageBox.Warning)
+                    if not res:
+                        return
+                    fname = Utilities.toNativeSeparators(fname)
+                
+                try:
+                    f = open(fname, "w", encoding="utf-8")
+                    itm = self.callTrace.topLevelItem(0)
+                    while itm is not None:
+                        isCall = itm.data(0, Qt.UserRole)
+                        if isCall:
+                            call = "->"
+                        else:
+                            call = "<-"
+                        f.write("{0} {1} || {2}\n".format(call,
+                            itm.text(1), itm.text(2)))
+                        itm = self.callTrace.itemBelow(itm)
+                    f.close()
+                except IOError as err:
+                    E5MessageBox.critical(self,
+                        self.trUtf8("Error saving Call Trace Info"),
+                        self.trUtf8("""<p>The call trace info could not be written"""
+                                    """ to <b>{0}</b></p><p>Reason: {1}</p>""")\
+                            .format(fname, str(err)))
     
     @pyqtSlot(QTreeWidgetItem, int)
     def on_callTrace_itemDoubleClicked(self, item, column):
         """
-        Slot documentation goes here.
+        Private slot to open the double clicked file in an editor.
+        
+        @param item reference to the double clicked item (QTreeWidgetItem)
+        @param column column that was double clicked (integer)
         """
-        # TODO: not implemented yet
-        raise NotImplementedError
+        if item is not None and column > 0:
+            columnStr = item.text(column)
+            if self.__entryRe.exactMatch(columnStr.strip()):
+                filename, lineno, func = self.__entryRe.capturedTexts()[1:]
+                try:
+                    lineno = int(lineno)
+                except ValueError:
+                    # do nothing, if the line info is not an integer
+                    return
+                if self.__projectMode:
+                    filename = self.__project.getAbsolutePath(filename)
+                self.sourceFile.emit(filename, lineno)
     
     def clear(self):
         """
@@ -117,6 +184,19 @@
         self.callTrace.clear()
         self.__callStack = []
     
+    def setProjectMode(self, enabled):
+        """
+        Public slot to set the call trace viewer to project mode.
+        
+        In project mode the call trace info is shown with project relative
+        path names.
+        
+        @param enabled flag indicating to enable the project mode (boolean)
+        """
+        self.__projectMode = enabled
+        if enabled and self.__project is None:
+            self.__project = e5App().getObject("Project")
+    
     def __addCallTraceInfo(self, isCall, fromFile, fromLine, fromFunction,
                            toFile, toLine, toFunction):
         """
@@ -136,16 +216,22 @@
             icon = UI.PixmapCache.getIcon("back.png")
         parentItem = self.__callStack[-1] if self.__callStack else self.callTrace
         
+        if self.__projectMode:
+            fromFile = self.__project.getRelativePath(fromFile)
+            toFile = self.__project.getRelativePath(toFile)
+        
         itm = QTreeWidgetItem(parentItem, ["",
             self.__entryFormat.format(fromFile, fromLine, fromFunction),
             self.__entryFormat.format(toFile, toLine, toFunction)])
         itm.setIcon(0, icon)
+        itm.setData(0, Qt.UserRole, isCall)
         itm.setExpanded(True)
         
         if isCall:
             self.__callStack.append(itm)
         else:
-            self.__callStack.pop(-1)
+            if self.__callStack:
+                self.__callStack.pop(-1)
     
     def isCallTraceEnabled(self):
         """
--- a/Debugger/CallTraceViewer.ui	Thu Nov 01 15:31:06 2012 +0100
+++ b/Debugger/CallTraceViewer.ui	Thu Nov 01 18:29:58 2012 +0100
@@ -86,9 +86,12 @@
      <property name="alternatingRowColors">
       <bool>true</bool>
      </property>
+     <property name="expandsOnDoubleClick">
+      <bool>false</bool>
+     </property>
      <column>
       <property name="text">
-       <string> </string>
+       <string/>
       </property>
      </column>
      <column>
--- a/Debugger/DebugUI.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/Debugger/DebugUI.py	Thu Nov 01 18:29:58 2012 +0100
@@ -1778,8 +1778,8 @@
                 
                 # Ask the client to send call trace info
                 enableCallTrace = self.debugViewer.isCallTraceEnabled()
-                if enableCallTrace:
-                    self.debugViewer.clearCallTrace()
+                self.debugViewer.clearCallTrace()
+                self.debugViewer.setCallTraceToProjectMode(debugProject)
                 
                 # Ask the client to open the new program.
                 self.debugServer.remoteLoad(fn, argv, wd, env,
@@ -1835,8 +1835,8 @@
             if self.lastStartAction in [1, 2]:
                 # Ask the client to send call trace info
                 enableCallTrace = self.debugViewer.isCallTraceEnabled()
-                if enableCallTrace:
-                    self.debugViewer.clearCallTrace()
+                self.debugViewer.clearCallTrace()
+                self.debugViewer.setCallTraceToProjectMode(forProject)
                 
                 # Ask the client to debug the new program.
                 self.debugServer.remoteLoad(fn, argv, wd, env,
--- a/Debugger/DebugViewer.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/Debugger/DebugViewer.py	Thu Nov 01 18:29:58 2012 +0100
@@ -190,6 +190,7 @@
         index = self.__tabWidget.addTab(self.callTraceViewer,
             UI.PixmapCache.getIcon("callTrace.png"), "")
         self.__tabWidget.setTabToolTip(index, self.callTraceViewer.windowTitle())
+        self.callTraceViewer.sourceFile.connect(self.sourceFile)
         
         # add the breakpoint viewer
         self.breakpointViewer = BreakPointViewer()
@@ -301,6 +302,17 @@
         """
         self.callTraceViewer.clear()
         
+    def setCallTraceToProjectMode(self, enabled):
+        """
+        Public slot to set the call trace viewer to project mode.
+        
+        In project mode the call trace info is shown with project relative
+        path names.
+        
+        @param enabled flag indicating to enable the project mode (boolean)
+        """
+        self.callTraceViewer.setProjectMode(enabled)
+        
     def showVariables(self, vlist, globals):
         """
         Public method to show the variables in the respective window.
--- a/Debugger/DebuggerInterfaceNone.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/Debugger/DebuggerInterfaceNone.py	Thu Nov 01 18:29:58 2012 +0100
@@ -325,6 +325,14 @@
         """
         return
         
+    def setCallTraceEnabled(self, on):
+        """
+        Public method to set the call trace state.
+        
+        @param on flag indicating to enable the call trace function (boolean)
+        """
+        return
+    
     def remoteEval(self, arg):
         """
         Public method to evaluate arg in the current context of the debugged program.
--- a/Debugger/DebuggerInterfacePython.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/Debugger/DebuggerInterfacePython.py	Thu Nov 01 18:29:58 2012 +0100
@@ -670,6 +670,18 @@
         self.__sendCommand('{0}{1:d}, "{2}"\n'.format(
             DebugProtocol.RequestSetFilter, scope, filter))
         
+    def setCallTraceEnabled(self, on):
+        """
+        Public method to set the call trace state.
+        
+        @param on flag indicating to enable the call trace function (boolean)
+        """
+        if on:
+            cmd = "on"
+        else:
+            cmd = "off"
+        self.__sendCommand('{0}{1}\n'.format(DebugProtocol.RequestCallTrace, cmd))
+    
     def remoteEval(self, arg):
         """
         Public method to evaluate arg in the current context of the debugged program.
@@ -805,6 +817,16 @@
                         self.debugServer.signalClientStack(stack)
                     continue
                 
+                if resp == DebugProtocol.CallTrace:
+                    event, fromStr, toStr = line[eoc:-1].split("@@")
+                    isCall = event.lower() == "c"
+                    fromFile, fromLineno, fromFunc = fromStr.rsplit(":", 2)
+                    toFile, toLineno, toFunc = toStr.rsplit(":", 2)
+                    self.debugServer.signalClientCallTrace(isCall,
+                        fromFile, fromLineno, fromFunc,
+                        toFile, toLineno, toFunc)
+                    continue
+                
                 if resp == DebugProtocol.ResponseThreadList:
                     currentId, threadList = eval(evalArg)
                     self.debugServer.signalClientThreadList(currentId, threadList)
--- a/Debugger/DebuggerInterfaceRuby.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/Debugger/DebuggerInterfaceRuby.py	Thu Nov 01 18:29:58 2012 +0100
@@ -622,6 +622,14 @@
         self.__sendCommand('{0}{1:d}, "{2}"\n'.format(
             DebugProtocol.RequestSetFilter, scope, filter))
         
+    def setCallTraceEnabled(self, on):
+        """
+        Public method to set the call trace state.
+        
+        @param on flag indicating to enable the call trace function (boolean)
+        """
+        return
+        
     def remoteEval(self, arg):
         """
         Public method to evaluate arg in the current context of the debugged program.
--- a/Project/Project.py	Thu Nov 01 15:31:06 2012 +0100
+++ b/Project/Project.py	Thu Nov 01 18:29:58 2012 +0100
@@ -2894,6 +2894,18 @@
         """
         return Utilities.fromNativeSeparators(self.getRelativePath(path))
         
+    def getAbsolutePath(self, fn):
+        """
+        Public method to convert a project relative file path to an absolute
+        file path.
+        
+        @param fn file or directory name to convert (string)
+        @return absolute path (string)
+        """
+        if not os.path.isabs(fn):
+            fn = os.path.join(self.ppath, fn)
+        return fn
+        
     def getAbsoluteUniversalPath(self, fn):
         """
         Public method to convert a project relative file path with universal

eric ide

mercurial