Thu, 01 Nov 2012 18:29:58 +0100
Finished the coding part of the call trace functionality.
--- 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