跳转至

DebuggerHelper - IDL 开发者的便捷调试类

原文链接: https://www.nv5geospatialsoftware.com/Learn/Blogs/Blog-Details/debuggerhelper-a-handy-debugging-class-for-idl-developers

21313 为此文章评分:

无评分

DebuggerHelper - IDL 开发者的便捷调试类

Jim Pendleton 2014年6月26日,星期四

还记得我们IDL中的老朋友——PRINT 过程吗?

除了忠实地展示我们导出的输出结果外,多年来它也帮我们解决了许多棘手的代码开发问题,尤其是在仅凭Workbench断点不够用时。是的,有时候连我的代码里也会出现错误,这得归功于宇宙射线、随机按键以及大脑突然卡壳。

然而,PRINT作为调试工具也有其缺点。以下是其中四个:

  1. 你必须输入"PRINT,",而且还有更多内容。这需要大量输入。(参见 Implied Print。)
  2. 除非你开发的应用程序只有一个用户,否则通常最终需要定位并注释掉或移除所有用于调试的PRINT语句。如果你不这样做,在客户、经理或论文委员会面前的现场演示中,IDL控制台选项卡上可能会出现可疑的注释。别问我是怎么知道的。
  3. 当你的Workbench或命令行窗口由于故意的不当行为(参见:bug)而关闭时,你会丢失任何本可以帮助你调试问题的PRINT输出,这些信息会在莫名其妙的逻辑中消失。
  4. 如果你分发IDL运行时应用程序(正如我们NV5定制解决方案团队经常做的那样),PRINT语句几乎没有什么价值,因为没有控制台输出。你需要一种方法从最终用户那里获取有用的信息,来补充"IDL崩溃了!"这种标准描述。

在多年的应用程序开发过程中,我逐渐形成了一种设计模式。我发现我真的需要一些有用的功能:

  1. 它必须易于使用,并且不能妨碍我的应用程序必须执行的真正工作
  2. 调试信息最好能显示在独立于IDL控制台的小部件中(如果需要)
  3. 调试输出最好能写入自动生成的、具有唯一名称的文本文件中(如果需要)
  4. 产生输出的命令输入起来应该比"PRINT,"更简单
  5. 这个工具应该是一个类,我可以实例化它,或者继承它来创建子类
  6. 该工具应该能自动生成时间戳和上下文信息
  7. 我应该能够在不修改源代码的情况下启用调试

现在,我将向你展示最终这个IDL类的简洁性,它提供了所有这些功能,却只有大约150行代码。代码在文章底部列出。你可以复制、粘贴并编译来跟着操作。

创建DebuggerHelper对象并开启调试

IDL> d = debuggerhelper(/on)

这会同时创建一个日志文件和一个调试文本小部件。实例化不能再简单了,这样就满足了我的前三个需求。

生成一行调试输出。

IDL> d.o, 'Hello bug'
(DEBUGGERHELPER) $MAIN$:Hello bug

需求4?满足。

我这里只展示了IDL控制台中的输出,但我同时也将信息写入了一个独立的文本小部件以及一个文本文件。显示的数据表明了输出是由哪个类产生的(可能是DebuggerHelper的子类),以及调用的上下文。在这个例子中,我们处于IDL命令行环境,所以显示为$MAIN$。

如果它是在一个IDL函数或过程内部被调用的,那么该例程的名称将替换$MAIN$,如下文所示。

我可以开启TICTOC 管理来帮助测试性能。这不是最初的需求,但在写这篇文章时我将其添加到了我的类中,因为实现起来非常简单。

IDL> d.tic, 'Timer 1'
IDL> d.o, 'How long did it take me to type this?', toc = 'Timer 1'
(DEBUGGERHELPER) $MAIN$, Performance timer 1 dt=21.950000:How long did it take me to type this?
IDL> d.o, 'That was not very fast.'
(DEBUGGERHELPER) $MAIN$:That was not very fast.

如果需要,我可以定义多个按名称区分的计时器来管理不同的代码段。

显然,对于内嵌在调试语句中的高性能测试,TIC和TOC不会那么精确,因为观察和记录的行为会干扰系统。然而,对于被测试操作所需时间显著超过调试I/O时间的情况,它们仍然有用。

我可以编程方式切换调试的开启和关闭,这意味着我不需要编辑源代码,这又满足了我的一个需求。

IDL> d.off
IDL> d.o, 'Do not criticize my typing speed'
IDL> d.on
IDL> d.o, 'Sorry about that'
(DEBUGGERHELPER) $MAIN$:Sorry about that

调试日志文件会自动生成并具有唯一名称,满足了另一个需求。让我们看看它被放在哪里了。

IDL> d.file
C:\mylib\logs\DEBUGGERHELPER1403199697.log

在这个实现中,日志文件是利用ROUTINE_FILEPATH 相对于源代码.pro文件(或SAVE文件)的位置创建的,但是在修改源代码或者从原始实现创建子类时,你可以根据自己的喜好进行调整。

即使在对象被销毁或IDL退出后,除非我主动删除文件,否则我仍然可以查看我的调试信息。

IDL> file = d.file
IDL> obj_destroy, d
IDL> b = strarr(file_lines(file))
IDL> openr,lun,file,/get_lun & readf, lun, b & free_lun, lun
IDL> b
[       11.310, 1403199708.570] (DEBUGGERHELPER) $MAIN$:Hello bug
[       66.141, 1403199763.401] (DEBUGGERHELPER) $MAIN$, Timer 1 dt=21.950000:How long did it take me to type this?
[       84.052, 1403199781.312] (DEBUGGERHELPER) $MAIN$:That was not very fast.
[      119.896, 1403199817.156] (DEBUGGERHELPER) $MAIN$:Sorry about that

请注意,日志文件包含了额外的、未打印到IDL控制台的时间戳信息。

我喜欢显示两个时间。第一个是自对象创建以来经过的秒数(相对时间)。对我来说,用这种方式格式化的数字做经过时间的脑力计算更容易。第二个时间是SYSTIME(1) 的输出。如果我的应用程序中有多个调试对象,以后我可以合并日志文件并按时间排序消息。例如,我可能想要对我应用程序不同部分使用的设备通信协议进行排序,甚至是在单独的独立应用程序或IDL_IDLBridge进程之间进行排序。

我可能想要求运行我应用程序的客户开启调试,以便我在代码中构建的内部语句能发挥作用。我们如何完全满足最后一个需求,即我不需要更改源代码就能在运行时IDL应用程序中启用调试呢?

GETENVCOMMAND_LINE_ARGS 提供了两种选择。例如,我可能会要求客户在启动我的IDL运行时应用程序之前,在他们的操作系统中创建一个环境变量,其名称是我在代码中硬编码的。在执行期间,我总是创建调试对象,但我会根据环境变量的设置来切换调试语句的开关,例如:

toDebug = getenv('DEBUG_IDL_APP') eq 'YES'

d = debuggerhelper(on = toDebug, no_file = ~toDebug)

在这里,如果用户没有创建一个名为DEBUG_IDL_APP的环境变量,或者他们没有用字符串"YES"来填充该环境变量,那么调试助手对象将被创建,但会保持静默。

甚至,在特定目录中放置或未放置一个文件,结合代码中相应的FILE_TEST调用,也可以用于此目的。

将调试对象添加到另一个类中,或从其继承创建子类,将提供额外的反馈。这里,我们将在新的"MyObj"类中创建一个DebuggerHelper对象作为成员变量。

IDL> .run
- function myobj::init
- return, 1
- end
% Compiled module: MYOBJ::INIT.
IDL> .run
- function myobj::init
- self.d = debuggerhelper(/on)
- return, 1
- end

IDL> .run
- pro myobj::yelp
- self.d.o, 'Within myobj'
- end
% Compiled module: MYOBJ::YELP.

创建该类的一个对象并调用生成调试输出的方法:

IDL> m = myobj()
IDL> m.yelp
(DEBUGGERHELPER) MYOBJ::YELP:Within myobj

请注意,产生调试输出的类名和方法名现在也显示在我的输出中了。

以下是DebuggerHelper的源代码IDL。我认为它很直接,但如果你有任何问题,请留言评论。我很乐意收到反馈,并且如果不仅仅是网络爬虫阅读我们的博客,我会对此有更好的感知。

Pro DebuggerHelper::_ConstructDebugWidget, Group_Leader
   Compile_Opt IDL2
   self._DebugTLB = Widget_Base(/Column, $
      Title = Obj_Class(self) + ' Debug', $
      Group_Leader = Group_Leader)
    DebugText = Widget_Text(self._DebugTLB, Value = '', $
      UName = 'DebugText', $
   XSize = 140, YSize = 24, /Scroll)
   Widget_Control, self._DebugTLB, /Realize
End
Pro DebuggerHelper::_CreateDebugFile
   Compile_Opt IDL2
   LogDir = FilePath('', $
      Root = File_DirName(Routine_Filepath()), $
      SubDir = ['logs'])
   If (~File_Test(LogDir)) then Begin
      File_MkDir, LogDir
      File_ChMod, LogDir, /A_Read, /A_Write
   EndIf
   self._DebugFile = FilePath(Obj_Class(self) + $
      StrTrim(ULong(SysTime(1)), 2) + '.log', $
      Root = LogDir)
   If (File_Test(self._DebugFile)) then Begin
      File_Delete, self._DebugFile
   EndIf
   OpenW, DebugLUN, self._DebugFile, /Get_LUN
   self._DebugLUN = DebugLUN
   File_ChMod, self._DebugFile, /A_Read, /A_Write
End
Pro DebuggerHelper::Cleanup
   Compile_Opt IDL2
   If (self._DebugLUN ne 0) then $
      Free_LUN, self._DebugLUN
   If (Widget_Info(self._DebugTLB, /Valid_ID)) then $
      Widget_Control, self._DebugTLB, /Destroy
End
Pro DebuggerHelper::DebugOutput, Output, $
   No_Print = NoPrint, $
   Up = Up, $
   Toc = Toc
   Compile_Opt IDL2
   If (~self._DebugOn) then Begin
      Return
   EndIf
   Elapsed = ''
   If (Toc ne !null) then Begin
      If (Size(Toc, /TName) eq 'STRING') then Begin
         Elapsed = ', ' + Toc + ' dt=' + $
            StrTrim(Toc(self._DebugClocks[Toc]), 2)
      EndIf Else If (Keyword_Set(Toc)) then begin
         Elapsed = ', dt=' + StrTrim(Toc(), 2)
      EndIf
   EndIf
   ThisClass = Obj_Class(self)
   Routines = (Scope_Traceback(/Structure)).Routine
   Result = '(' + ThisClass + ') ' + $
      Routines[N_elements(Routines) - $
      (2 + Keyword_Set(Up))] + $
      Elapsed + ':' + Output
   If (~Keyword_Set(NoPrint)) then Begin
      Print, Result, Format = '(a)'
   EndIf
   If (~Widget_Info(self._DebugTLB, /Valid_ID)) then Begin
      Return
   EndIf
   Now = SysTime(1)
   DT = '[' + $
      String(Now - self._DebugCreationTime, $
         Format = '(f14.3)') + ', ' + $
         String(Now, Format = '(f14.3)') + $
         '] '
   DebugText = Widget_Info(self._DebugTLB, $
      Find_by_UName = 'DebugText')
   DebugYSize = (Widget_Info(DebugText, /Geometry)).YSize
   Widget_Control, DebugText, Get_UValue = NLines
   If (N_elements(NLines) eq 0) then Begin
      NLines = 0L
   EndIf
   Line = DT + Result
   If (self._DebugLUN ne 0) then Begin
      PrintF, self._DebugLUN, Line
      Flush, self._DebugLUN
   EndIf Else Begin
      Print, Line, Format = '(a)'
   EndElse
   If (NLines gt self._DebugWindowMaxLines) then Begin
      Widget_Control, DebugText, Get_Value = Old
      Lines = Old[self._DebugWindowMaxLines/2:*]
      Old = 0
      NLines = N_elements(Lines)
      Widget_Control, DebugText, $
         Set_Value = [Temporary(Lines), Line]
      Widget_Control, DebugText, $
         Set_UValue = NLines + 1L
   EndIf Else Begin
      Widget_Control, DebugText, $
         Set_Value = Line, /Append
      Widget_Control, DebugText, $
         Set_UValue = ++NLines
   EndElse
   Widget_Control, DebugText, Set_Text_Top_Line = $
      NLines - DebugYSize + 3 > 0
End
Pro DebuggerHelper::O, Output, _Extra = Extra
   Compile_Opt IDL2
   self->DebugOutput, Output, /Up, _Extra = Extra
End
Pro DebuggerHelper::Off
   Compile_Opt IDL2
   self._DebugOn = 0
End
Pro DebuggerHelper::On
   Compile_Opt IDL2
   self._DebugOn = 1
End
Pro DebuggerHelper::Tic, Name
   Compile_Opt IDL2
   self._DebugClocks += Hash(Name, Tic(Name))
End
Pro DebuggerHelper::GetProperty, $
   On = On, $
   File = File
   Compile_Opt IDL2
   If (Arg_Present(On)) then On = self._DebugOn
   If (Arg_Present(File)) then $
      File = self._DebugFile
End
Function DebuggerHelper::Init, $
   On = On, $
   Group_Leader = Group_Leader, $
   No_File = No_File
   Compile_Opt IDL2
   self._DebugCreationTime = SysTime(1)
   self._DebugWindowMaxLines = 500
   self._DebugOn = Keyword_Set(On)
   self._DebugClocks = Hash()
   self->_ConstructDebugWidget, Group_Leader
   If (~Keyword_Set(No_File)) then $
      self->_CreateDebugFile
   Return, 1
End
Pro DebuggerHelper__Define
   !null = {DebuggerHelper, Inherits IDL_Object, $
   _DebugOn : 0B, $
   _DebugLUN : 0L, $
   _DebugFile : '', $
   _DebugTLB : 0L, $
   _DebugCreationTime : 0D, $
   _DebugClocks : Hash(), $
   _DebugWindowMaxLines : 0L}
End

免费遥感数据 云计算在遥感应用中的优势