Pages

Advertisement

Wednesday, July 11, 2007

Happy Breakpoints for Testing!

The other day I was testing a bunch of code for my upcoming book Debugging Microsoft .NET and Windows Applications (Microsoft Press) and found myself wishing there was a way to set breakpoints on all the functions in a particular source file. In order to initially verify that my unit test was actually doing anything worthwhile, I wanted to at least ensure I was executing each method in a particular file.

Since I was working with a new set of code, I was doing the initial testing and scrolling like mad so I could click in the margin next to each function to get that breakpoint set. After I'd scrolled half way through the file, I was cursing at myself thinking that there had to be a better way. After I got done with the initial set of testing, I simply had to take a look at finding a way to automate setting breakpoints for a file no matter what language I was using. At the rate I was going, I was going to wear the bottom off my poor optical mouse scrolling all over the place. Fortunately, the very cool extensibility model with Visual Studio .NET actually made it relatively easy to achieve my goal. Even better was instead of grinding through and doing an Add-In, it was something that was easily accomplished with a macro. The icing on the cake is that with the code at the end of this article, you can set, and more importantly, remove, those function breakpoints without messing up any of your carefully set existing breakpoints!

There's two pieces of work necessary to achieve the goal. The first is to figure out how to find each function/method in a file. The second part is setting and removing the breakpoints. Finding the particular functions in a source file can be a daunting task involving parsers and all sorts of weird technologies such as FLEX and YACC. If you've ever worked on real parsers you know they are extremely hard to do. In fact, since the premise behind .NET is the language independence, there's no telling how many languages you might have to write parsers for simply to find the function locations. While it would be nice to spend the next five years working on parsers, I just wanted a simple utility!

The good news is that Visual Studio .NET does all the heavy lifting for you and essentially hands you the parse tree for the current document through a very clean interface. If you search the MSDN for "Automation Object Model Chart" you'll find the chart that shows you a set of very cool objects such as called CodeElement, CodeFunction, CodeClass, and CodeEnum. By enumerating and recursing these elements you'll get the complete layout of what's in a particular source file. Keep in mind that the source file enumeration is only available on files that are part of the open project. Given the fact that this works for all languages is such a huge feature I'm sure people will be doing all sorts of very cool tools that we always wished for in the past but didn't have the time to write a complete parser.

If something as complicated as the parsing is already done for you, having complete access to the Debugger object to set or clear breakpoints is almost anticlimactic. The algorithm for setting breakpoints on all function entry is the following:

Get the code elements for the active document in the project
for each code element
{
Is the element a Namespace, Class or Struct?
{
Recurse the child elements
}
Is the element a function or property?
{
Get the line where this element starts
Set a file and line breakpoint at that location
}
}

One thing I want to point out about SetBreakpointsOnAllCurrentDocFunctions is that after you run it, you'll see the breakpoints set, but they might look like they are in the wrong place. For example, in a .NET program the breakpoint can be sitting on an attribute before the function. That's perfectly fine as once you start debugging the debugger does the exact right thing and moves the breakpoint down to the first executing line of the function.

After I whipped up the first version of the SetBreakpointsOnAllCurrentDocFunctions macro, it ran just like I expected. However, further testing showed up some problems, not in my code, but in Visual Studio .NET that I need to make you aware of. The worst problem is with C++ header files. For some reason, nearly everything in the file is marked as a function and the starting and ending points for the elements don't relate too reality in the file. I played around with it quite a bit and considered not processing header files, but since many people do put inline functions, I decided against it. What you'll see are a bunch of breakpoints on empty lines and in comments but the good news is that you can forget about them because the debugger will ignore them as they can't be set.

The second issue I found was that some C++ source files are not properly parsed by the environment and might be missing a function or two in the code model. In those cases, there's nothing you can do to get the actual function unless you want to grind through the file yourself. The good news is that it's not something you'll run into very much. For those of you doing primarily .NET development everything lines up perfectly with Visual Basic .NET and C#.

The last issue I ran into was what got me thinking that I needed a way to easily remove any breakpoints put in by SetBreakpointsOnAllCurrentDocFunctions. If you click on the red dot breakpoint marker in the source file, you'll find that it will never toggle off. You can clear the breakpoint by either right clicking on it and selecting Remove from the context menu or clearing it from the Breakpoint window.

If I was going to be setting all these breakpoints automatically, I simply had to have a way to clear them out. While I could have grabbed the breakpoints collection from the Debugger object and wiped it clear, having a macro remove your carefully placed breakpoints isn't that useful. In reading about the Breakpoint object, I saw that Microsoft was really thinking ahead and gave us a Tag property where we could squirrel away a user defined string! All I had to do was uniquely identify any breakpoints I set and they'd be a piece of cake to remove. The one worry I had was that the Tag field wouldn't have been saved between sessions, but a little experimentation proved it was. For the tag, I use the filename as part of it so when you run the RemoveBreakpointsOnAllCurrentDocFunctions macro it only removes the breakpoints put in by SetBreakpointsOnAllCurrentDocFunctions for the active file and leaves any others you set in that file alone.

Armed with SetBreakpointsOnAllCurrentDocFunctions and RemoveBreakpointsOnAllCurrentDocFunctions I've found that my testing is going easier because fewer than 200 lines of macro code quickly automate something I was doing manually all the time. Now you can easily find out if all the functions in a file are being called. As you hit each function, clear the breakpoint. At the end of the run, you'll see exactly which functions haven't been called. Good luck and crank that code coverage.

About the Author

John Robbins is the co-founder of Wintellect (http://www.wintellect.com), a consulting, debugging, and education firm that helps client's ship better code faster. He is also the author of Debugging Microsoft .NET and Windows Applications (Microsoft Press) as well as the Bugslayer columnist for MSDN Magazine. Before founding Wintellect, John was an architect and product manager at NuMega Technologies for products such as BoundsChecker, TrueTime, and TrueCoverage. Prior to joining the software world, John was a Paratrooper and Green Beret in the U. S. Army.

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' BreakPoints Module
'
' John Robbins - Wintellect - http://www.wintellect.com
'
' A module that will set and clear breakpoints at the entry point of
' all functions in the current source file. What's even cooler is
' that this code will not screw up breakpoints you already have set!
' Additionally, when removing breakpoints, it will only remove the
' breakpoints put for the current source file.
'
' There are some caveats:
' 0. The breakpoints set by the SetBreakpointsOnAllCurrentDocFunctions
' macro show up as you'd expect in the source windows as a red dot
' next to the line where they were set. However, you can click on
' that dot all day long as it will not clear it. Either run
' RemoveBreakpointsOnAllCurrentDocFunctions or clear them from the
' Breakpoints window. This seems to be a bug in the IDE.
' 1. There's a bug in the CodeModel for C++ header files. Pretty much
' anything in one gets called a function. There's no clean way to
' double check, short of parsing the file yourself, if the
' TextPoint values are real. If you run this on a header, you'll
' get breakpoints all over the place. Fortunately, the debugger
' is smart enough to ignore them.
' 2. The breakpoints are set at what the CodeElement.StartPoint
' property says is the first line. This can be at the start of an
' attribute or something. Don't worry, the debugger does the right
' thing and moves the breakpoint to the first executable line '
' inside the function. (Go Microsoft!) If a .NET method is empty,
' the breakpoint is set on the end of the function.
' 3. There's an odd bug you might run into when debugging this code.
' After setting the breakpoint, I access it to set the Tag field so
' I can identify which breakpoints this macro set. When debugging,
' that access seems to cause a Null Reference exception in some
' cases. However, if you don't set breakpoints, it will run fine.
' 4. In some C++ source files, the CodeModel occasionally does not
' have a function or two that's shown in the code window. Since
' you can't get them, you can't set breakpoints on them.
' 5. The active document returned from DTE.ActiveDocument is odd.
' It's the last code document that had focus. This can mean you're
' looking at the Start Page, but setting breakpoints on something
' hidden. These macros force you to have the cursor in a real code
' window before they will run.
'
' Version 1.0 - August 28, 2002
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''




Imports EnvDTE
Imports System.Diagnostics
Imports System.Collections
Public Module BreakPoints
Const k_ConstantTagVal As String = "Wintellect Rocks "
Public Sub SetBreakpointsOnAllCurrentDocFunctions()
' Get the current source file name doing all the checking.
Dim CurrDoc As Document = GetCurrentDocument()
If (CurrDoc Is Nothing) Then
Exit Sub
End If
' Get the source file name and build up the tag value.
Dim SrcFile As String = CurrDoc.FullName
Dim TagValue As String = BuildTagValue(CurrDoc)


' While I might have a document, I still need to check this
' is one I can get a code model from.




Dim FileMod As FileCodeModel = 
CurrDoc.ProjectItem.FileCodeModel
If (FileMod Is Nothing) Then
MsgBox("Unable to get code model from document.", _
MsgBoxStyle.OKOnly, _
k_ConstantTagVal)
Exit Sub
End If
' Everything's lined up to enumerate!
ProcessCodeElements(FileMod.CodeElements, SrcFile, TagValue)
End Sub
Private Sub ProcessCodeElements(ByVal Elems As CodeElements, _
ByVal SrcFile As String, _
ByVal TagValue As String)
' Look at each item in this collection.
Dim CurrElem As CodeElement
For Each CurrElem In Elems
' If I'm looking at a class, struct or namespace, I need 
' to recurse.
If (vsCMElement.vsCMElementNamespace = CurrElem.Kind) Or _
(vsCMElement.vsCMElementClass = CurrElem.Kind) Or _
(vsCMElement.vsCMElementStruct = CurrElem.Kind) Then
' This is kinda odd. Some CodeElements use a Children
' property to get sub elements while others use 
' Members.
Dim SubCodeElems As CodeElements = Nothing
Try
SubCodeElems = CurrElem.Children
Catch
Try
SubCodeElems = CurrElem.Members
Catch
SubCodeElems = Nothing
End Try
End Try
If (Not (SubCodeElems Is Nothing)) Then
If (SubCodeElems.Count > 0) Then
ProcessCodeElements(SubCodeElems, _
SrcFile, _
TagValue)
End If
End If
ElseIf (CurrElem.Kind = _
vsCMElement.vsCMElementFunction) Or _
(CurrElem.Kind = _
vsCMElement.vsCMElementProperty) Then
' Interestingly, Attributed COM component attributes 
' show up broken out into their functions. The only
' thing is that their StartPoint property is invalid
' and throws an exception when accessed.
Dim TxtPt As TextPoint
Try
TxtPt = CurrElem.StartPoint
Catch
TxtPt = Nothing
End Try
If (Not (TxtPt Is Nothing)) Then
Dim LineNum As Long = TxtPt.Line
Dim Bps As EnvDTE.Breakpoints
' Plop in one of my breakpoints.
Bps = DTE.Debugger.Breakpoints.Add(File:=SrcFile, _
Line:=LineNum)
' Get the BP from the collection and set the tag
' property so I can find the ones I set.
Try
' There's some sort of bug here. If you debug
' through this with the VSA debugger, it fails
' (0x8004005's) on accessing the breakpoints 
' collection occasionally. However, if you 
' run it, life is good. Whateva!
Dim Bp As EnvDTE.Breakpoint
For Each Bp In Bps
Bp.Tag = TagValue
Next
Catch
End Try
End If
End If
Next
End Sub
Public Sub RemoveBreakpointsOnAllCurrentDocFunctions()
' This is a much simpler function since I set the tag value on
' the breakpoints, I can remove them simply by screaming 
' through all BPs and removing those.
Dim CurrDoc As Document = GetCurrentDocument()
If (CurrDoc Is Nothing) Then
Exit Sub
End If
Dim TagValue As String = BuildTagValue(CurrDoc)
Dim CurrBP As EnvDTE.Breakpoint
For Each CurrBP In DTE.Debugger.Breakpoints
If (CurrBP.Tag = TagValue) Then
CurrBP.Delete()
End If
Next
End Sub
Private Function GetCurrentDocument() As Document
' Check to see if a project or solution is open. If not, you
' can't get at the code model for the file.
Dim Projs As System.Array = DTE.ActiveSolutionProjects
If (Projs.Length = 0) Then
MsgBox("You must have a project open.", _
MsgBoxStyle.OKOnly, _
k_ConstantTagVal)
GetCurrentDocument = Nothing
Exit Function
End If
' Getting the active document is a little odd.
' DTE.ActiveDocument will return the active code document, but
' it might not be the real ACTIVE window. It's quite 
' disconcerting to see macros working on a document when you're
' looking at the Start Page. Anyway, I'll ensure the active 
' document is really the active window.
Dim CurrWin As Window = DTE.ActiveWindow
Dim CurrWinDoc As Document = CurrWin.Document
Dim CurrDoc As Document = DTE.ActiveDocument
' Gotta play the game to keep from null ref exceptions in the 
' real active doc check below.
Dim WinDocName As String = ""
If Not (CurrWinDoc Is Nothing) Then
WinDocName = CurrWinDoc.Name
End If
Dim DocName As String = "x"
If Not (CurrDoc Is Nothing) Then
DocName = CurrDoc.Name
End If
If ((CurrWinDoc Is Nothing) And _
(WinDocName <> DocName)) Then
MsgBox("The active cursor is not in a code document.", _
MsgBoxStyle.OKOnly, _
k_ConstantTagVal)
GetCurrentDocument = Nothing
Exit Function
End If
' While I might have a document, I still need to check this is
' one I can get a code model from.
Dim FileMod As FileCodeModel = 
CurrDoc.ProjectItem.FileCodeModel
If (FileMod Is Nothing) Then
MsgBox("Unable to get code model from document.", _
MsgBoxStyle.OKOnly, _
k_ConstantTagVal)
GetCurrentDocument = Nothing
Exit Function
End If
GetCurrentDocument = CurrDoc
End Function
Private Function BuildTagValue(ByVal Doc As Document) As String
BuildTagValue = k_ConstantTagVal + Doc.FullName
End Function
End Module

No comments:

Post a Comment