In a nutshell, the Trace-Match methodology is the ability to generate
a trace file both in the original VB6 code and in the migrated .NET code, and later
compare the two results to verify that the original project and the migrated project
behave in the same way when the same sequence of actions is performed on them.
While the Trace-Match methodology alone can’t warrant full functional equivalence,
for sure it is a powerful weapon in the hands of developers wishing to complete
the migration process as quickly and correctly as possible.
We at Code Architects have successfully used the Trace-Match methodology for our
migration services and have refined it over time. Starting with VB Migration Partner
1.32, we are releasing this methodology to the public, so that all our customers
can benefit from it.
The steps to correctly implement the Trace-Match methodology are few and simple:
-
Instrument the original VB6 project with trace statements
-
Refine the trace mechanism and insert additional trace commands (optional)
-
Run the original VB6 project and produce a set of trace files
-
Convert the VB6 project to .NET
-
Run the same tests on the migrated .NET project and produce a set of trace files
-
Compare the VB6 and .NET trace files
-
Removing trace statements
1. Instrument the original VB6 project with trace statements
Adding trace statements to a VB6 project is quite easy: just load a VB6 project
inside VB Migration Partner (version 1.32 or later) and invoke the Insert Trace
statement command from the Tools menu.
Once the operation is completed, the following message box appears:
IT IS ESSENTIAL THAT YOU HAVE PREPARED A BACKUP COPY OF YOUR VB6 PROJECT,
because even if the Tools menu contains the Remove Trace statements
command, it isn’t guaranteed that it works well under all possible circumstances.
Besides, this command doesn’t automatically undo all the manual refinements
you have done (see next section).
If you are sure that you have a backup copy of your project, click “Yes”
and wait until the VB6 project is saved and reloaded.
The Insert Trace statements command performs the following actions:
- It modifies the VBP file to add a reference to VB6TraceLib.dll type library, which
includes two tracing classes (VB6AppTrace and VB6MethodTrace)
- If the project includes at least a BAS module, it includes the following declaration
inside that module:
Public AppTrace As New VB6AppTrace
If the project doesn’t contain any BAS module, VB Migration Partner automatically
adds a module named VBMigrationPartner_AppTrace.bas.
- It adds trace statements to all methods in the application.
For example, let’s say that the application contains a Module1.Bas with this
code:
Sub Test(ByRef x As String)
If x = "" Then
Exit Sub
End If
DoSomething x
End Sub
This is what the module looks like after trace statements have been inserted (added
statements are in boldface)
Public AppTrace As New VB6AppTrace
Sub Test(ByRef x As String)
Dim trace_ As VB6MethodTrace: Set trace_ = AppTrace.EnterMethod("Module1.Test", x)
If x = "" Then
AppTrace.ExitMethod trace_, x: Exit Sub
End If
DoSomething x
AppTrace.ExitMethod trace_, x
End Sub
NOTE 1: Trace statements only include simple parameters (strings,
numbers, dates, etc.) Object, array, and UDT parameters aren’t included in
the list of traced values.
NOTE 2: in rare circumstances, VB Migration Partner may incorrectly
modify the VBP project file (see point A above) and the reference to the VB6TraceLib.dll
isn’t valid. When this happens, you need to manually add a reference to the
VB6TraceLib.dll file, which you can find in VB Migration Partner’s setup folder.
You can now exit VB Migration Partner and load the modified project inside the Visual
Basic 6 IDE.
2. Refine the trace mechanism and add additional trace commands (optional)
The Insert Trace statements command saves you the drudgery of manually
inserting trace commands in each and every method of your project, yet the code
it produces uses the default trace settings. In many cases you might want to refine
the generated code to precisely specify how the trace should look like.
The first kind of refinement consists of correctly initializing the VB6AppTrace
object. This operation is typically done in the Sub Main method, or in the Form_Load
event handler of the startup form:
Sub Main()
AppTrace.Init IndentTrace Or IncludeImplicit Or ShowTime, ""
...
End Sub
The Init method accepts three arguments, all of which are optional.
Trace options
The first argument for the AppTrace.Init method is an enumerated value that specifies
which tracing options should be enabled. Valid values are:
None: all options are disabled (this is the default behavior)
IndentTrace: indent trace output to reflect method call nesting
ShowTime: include time in trace output (number of seconds from
when trace was initialized)
IncludeTerminate: include output from Class/Form Terminate methods
IncludeImplicit: include trace statements that mark when a method
is exited implicitly (e.g. because of an unanticipated error)
OmitFile: don't send to trace file
Two additional available values are specific for .NET and have no effect under VB6:
AutoGC: invoke the GC.Collect method when EnterMethod is invoked
FastWrite: non-cached file writes
The default behavior is such that trace files produced under VB6 are easily comparable
with trace files generated under .NET by means of most file comparison (Diff) utilities,
such as WinMerge. (See
this page for a complete list of free Diff tools.)
The IndentTrace option provides a better understanding of execution
flow, but may cause several “false unmatches” when comparing files using
a Diff tool. The ShowTime option provides a very basic profiling
feature and is guaranteed to generate a different output under VB6 and .NET.
The IncludeTerminate and IncludeImplicit options
call for an additional explanation. The VB6 tracing mechanism leverages the fact
that a VB6/COM object fires its Class_Terminate event as soon as the object goes
out of scope. This feature is dubbed deterministic finalization and the
VB6TraceLib library uses it to achieve to important goals:
- trace when exactly an object is destroyed, thanks to the trace statements in its
Class_Terminate event handler
- trace when any method exits because of an unhandled error. (In this case the auxiliary
VB6MethodTrace object goes out of scope and fires its Class_Terminate event, thus
the VB6TraceLib has an opportunity to detect that the method exited.)
As you may know, in the .NET Framework object destruction isn’t deterministic,
and objects are finalized some time after they go out of scope or are explicitly
set to Nothing. Because of this difference, the .NET version of the VB6AppTrace
class can’t rely on deterministic finalization to detect when an object is
destroyed or a method exits because of an error. For this reason, the IncludeTerminate
or IncludeImplicit options often generate .NET trace files that somehow differ from
the VB6 trace file.
We have adopted many advanced programming techniques to reduce the
number of differences between VB6 and .NET trace files. (More details later in this
document.)
Finally, the OmitTrace flag omits writing to trace file. In this
case, trace output from the VB6 or .NET application can be intercepted by means
of DebugView
or similar utilities. This feature can be very helpful for debug and test purposes,
not just when implementing the Trace-Match methodology.
Exclude pattern
The second argument to the AppTrace.Init method is an optional
regular expression pattern that defines which methods should be excluded from tracing.
For example, you may want not to include tracing from inside MouseDown, MouseMove,
and MouseUp event handlers, which you can easily achieve with this code:
AppTrace.Init , "_Mouse(Down|Move|Up)$"
The name of the method being traced is in the format “classname.methodname”,
thus for example the MouseMove hander for the Text1 control on the Form1 form appears
as "Form1.Text1_MouseMove" in the trace file. The above regular expression
discards trace for any method whose name ends with “MouseDown”, “MouseMove”,
or “MouseUp”.
Excluding all the methods from a specific form, class, or module is equally easy.
For example, the following code disables trace inside all the methods inside the
Widget class:
AppTrace.Init , "^Widget\."
Notice that the exclude pattern only works at runtime. In other words, VB Migration
Partner always adds trace statements to all the methods of all files in the current
project or project group, but you can decide which methods should not emit
trace output when you run the VB6 (or .NET) project.
NOTE: the VB6TraceLib.dll component uses the Microsoft VBScript
Regular Expression 5.5 type library to implement the exclude feature.
If such a library isn’t installed on the current machine, specifying a nonempty
string for the ExcludePattern argument causes a runtime error.
Trace file name
The third optional argument to the AppTrace.Init method is the
name of the output file. By default this file is C:\TRACE_VB6.TXT, but you can specify
another file if you wish:
AppTrace.Init , "^Widget\.", "c:\tracefiles\test_VB6.txt"
When you later convert this code to .NET and run the migrated project,
the corresponding VB6AppTrace.Init method in the VB Migration Partner’s support
library automatically changes “VB6” (uppercase) into “NET”.
This means that this code will create the test_VB6.txt file under
VB6 and the test_NET.txt file in the migrated project. You never
need to worry that the converted .NET project might accidentally overwrite the VB6
trace file.
Additional trace output
In addition to the statements automatically added by the Insert Trace statements
command, you are free to decorate the existing VB6 code with additional trace output
calls. Both the VB6AppTrace and VB6MethodTrace objects expose the Trace method,
therefore you can use either of the following syntaxes:
Sub Test(ByRef x As String)
Dim trace_ As VB6MethodTrace: Set trace_ = AppTrace.EnterMethod("Module1.Test", x)
...
AppTrace.Trace "just before calling DoSomething"
DoSomething
trace_.Trace "just after calling DoSomething"
AppTrace.ExitMethod trace_, x
End Sub
The output from these two methods is only slightly different: the former syntax
emits the string as-is, whereas the latter one prefixes the text with the method
name:
just before calling DoSomething
<trace from DoSomething method here>
Module1.Test: just after calling DoSomething
You can use either form of Trace method to trace the values of your variables, the
state of the running program, details on the execution flow, and any other piece
of information that is useful to understand what happens inside the application
about to be migrated to .NET.
Selectively enable/disable tracing
Another way to improve the quality of your trace files is to selectively disable
(and then re-enable) tracing in portions of code that are of no interest for you.
For example, let’s assume that you have thoroughly tested the DoSomething
method and that you are sure that it works correctly. If this is the case, you might
want to disable tracing before calling that method, and re-enable it immediately
afterwards:
Sub Test(ByRef x As String)
Dim trace_ As VB6MethodTrace: Set trace_ = AppTrace.EnterMethod("Module1.Test", x)
...
AppTrace.Enabled = False
DoSomething
AppTrace.Enabled = True
AppTrace.ExitMethod trace_, x
End Sub
3. Run the original VB6 project and produce a set of trace files.
Conceptually this step is very simple: you just run a well-defined set of actions
on the original VB6 application and produce one or more trace files.
We recommend that you prepare a script of these actions, with details such as: what
menu commands you select; which push buttons you click (and whether you activate
them using the mouse or the keyboard); which characters you enter in fields, and
so forth. It is important that you precisely describe these actions, because you’re
going to replicate them in the converted .NET application.
We also recommend that you run the VB6 project as a compiled stand-alone file, because
the behavior of interpreted and compiled VB6 projects can sometime differ.
Finally, we suggest that you produce a separate trace file for each case test. There
are two ways to produce multiple trace files from the same project:
- you modify the third argument of the VB6AppTrace.Init method before
each run. (This technique forces you to edit and recompile the VB6 source code for
each trace file.)
- you rename the resulting trace file at the end of each test case from Windows Explorer,
so that the file isn’t overwritten when you run the next test case. (This
approach is usually the most convenient one.)
Here’s a very short example of trace file that might be produced by a simple
VB6 application that has one form (Form1) and one class (Widget). The trace was
obtained by loading Form1 and then clicking on its two button (Command1 and Command2),
where the Command2_Click event handler creates an instance of the Widget class and
invokes its One method, which in turn invokes its Two method. Notice that the IndentLevel,
ShowTime, and IncludeTerminate options are all enabled.
0000.00: --- START -------------------------
0000.00: Enter Form1.Form_Load
0000.00: Exit Form1.Form_Load
0003.77: Enter Form1.Command1_Click
0003.77: Enter Form1.DoSomething
0003.77: Enter Form1.DoSomethingElse
0003.77: Exit Form1.DoSomethingElse
0003.77: Exit Form1.DoSomething
0003.77: Exit Form1.Command1_Click
0005.02: Enter Form1.Command2_Click
0005.02: Enter Widget.One
0005.02: Enter Widget.Two
0005.02: Exit Widget.Two
0005.02: Exit Widget.One
0005.02: Exit Form1.Command2_Click
0005.02: **Enter Widget.Class_Terminate
0005.02: ** Enter Widget.Two
0005.02: ** Exit Widget.Two
0005.02: **Exit Widget.Class_Terminate
0007.05: Enter Form1.Form_Unload
0007.05: Exit Form1.Form_Unload
0007.06: --- END -------------------------
The lines that contain two asterisks are produced by calls that originate from inside
a Terminate event (in this case the Class_Terminate event of the Widget class).
By comparison, this is the trace file produced by the same series of actions when
no trace option is enabled:
--- START -------------------------
Enter Form1.Form_Load
Exit Form1.Form_Load
Enter Form1.Command1_Click
Enter Form1.DoSomething
Enter Form1.DoSomethingElse
Exit Form1.DoSomethingElse
Exit Form1.DoSomething
Exit Form1.Command1_Click
Enter Form1.Command2_Click
Enter Widget.One
Enter Widget.Two
Exit Widget.Two
Exit Widget.One
Exit Form1.Command2_Click
Enter Form1.Form_Unload
Exit Form1.Form_Unload
--- END -------------------------
As you see, calls that originate from the Class_Terminate event of the Widget class
aren’t displayed any longer and that the lack of indentation makes it more
difficult to follow the execution path.
4. Convert the VB6 project to .NET
Nothing special here: this is standard stuff for all VB Migration Partner users.
It is essential, however that you strictly adhere to the
convert-test-fix methodology we recommend in our documentation. This methodology
dictates that you never modify the converted .NET code to solve compilation and
runtime errors; instead, you add one or more pragmas to the original VB6 source
code and then re-convert the project to .NET.
NOTE: Because pragmas are just special VB6 comments, the convert-test-fix
approach ensures that the trace files you have created at previous steps continue
to be valid and correctly describe how the VB6 code behaves.
This step completes when you have a .NET project that compiles correctly and run
without any runtime exception. Unfortunately, running without errors doesn’t
necessarily mean that the .NET code is functionally equivalent to the original VB6
project.
Ensuring functional equivalence is the purpose of the Trace-Match methodology.
5. Run the same tests on the migrated .NET project and produce a set of trace files
Now that the .NET code runs with no visible exceptions, you can perform the same
set of scripts and test cases seen in step 3, and produce a similar set of trace
files.
6. Compare the VB6 and .NET trace files
You now have all the trace information you need, both from the original VB6 project
and the converted .NET project. Using a Diff tool like WinMerge it’s easy
to spot any major and minor difference between the VB6 and .NET trace files.
Notice that a difference in trace files doesn’t really give you the absolute
certainty that the converted code isn’t functionally equivalent to the original
project. For example, the .NET trace file might look like this:
0000.00: --- START -------------------------
0000.00: Enter Form1.Form_Load
0000.00: Exit Form1.Form_Load
0003.22: Enter Form1.Command1_Click
0003.22: Enter Form1.DoSomething
0003.22: Enter Form1.DoSomethingElse
0003.22: Exit Form1.DoSomethingElse
0003.22: Exit Form1.DoSomething
0003.22: Exit Form1.Command1_Click
0004.81: Enter Form1.Command2_Click
0004.81: Enter Widget.One
0004.81: Enter Widget.Two
0004.81: Exit Widget.Two
0004.81: Exit Widget.One
0004.81: Exit Form1.Command2_Click
0004.81: Enter Form1.Form_Unload
0005.15: Exit Form1.Form_Unload
0005.22: **Enter Widget.Class_Terminate
0005.22: ** Enter Widget.Two
0005.22: ** Exit Widget.Two
0005.22: **Exit Widget.Class_Terminate
Even not considering the difference in timing, the most obvious difference with
the original VB6 trace file (see step 3 above) is that calls originated from the
Class_Terminate event in the Widget class come after the Form_Unload method in the
.NET code, whereas they followed the exit from Command1_Click method in the VB6
project.
The reason for this difference is that .NET objects are usually destroyed later
than the corresponding VB6 object. Worse, if the object is being destroyed when
the entire application is shutting down you might not see any trace from their Class_Terminate
event handler.
NOTE: For the same reason, you might not see the “---- END
--- “ line in trace files produced under .NET. In fact, the VB6AppTrace object
might be destroyed before the last operation.
To avoid false mismatches caused by undeterministic finalization, by default
VB Migration Partner’s trace mechanism omits trace output originated from
inside the Terminate event handler of forms, classes, and user controls. However,
if you specified the IncludeTerminate option (see step 2 above),
this output is included in the trace file but is marked with a double asterisks.
Alternatively, you can decide to enable the IncludeTerminate option and later use
a Grep utility to discard those lines from the output. For example, you might use
the FIND utility from Windows command line, as follows:
FIND c:\trace_net.txt "**" /V
Forced garbage collections
As explained previously, the different finalization mechanism explains why the VB6
and .NET versions of the trace file often differ. A simple trick that greatly reduces
these differences is the AutoGC trace option, which you enable
with this code (in the original VB6 project):
AppTrace.Init IndentTrace Or IncludeImplicit Or AutoGC
As its name suggests, the AutoGC option forces a full garbage collection
each time a method is exited (that is, when the VB6AppTrace.ExitMethod is called).
Such forced garbage collections reduces the gap between deterministic (VB6) and
undeterministic (.NET) finalization, which in turn reduces the differences between
VB6 and .NET trace files.
Keep in mind, however, that even when you enable this option, the deep differences
between the two finalization mechanisms aren’t solved completely. Also, each
garbage collection adds overhead to your code, so be prepared for higher execution
timings when this option is enabled.
7. Removing trace statements
Once you are 100% sure that the converted code behaves exactly like the original
VB6 project and that functional equivalence has been reached, you can remove all
trace statements from the VB6 project and perform the final conversion, to produce
a fully working .NET project that contains no trace statements.
This step is fully automatic, thanks to the Remove Trace statements
command from VB Migration Partner’s Tools menu.
If you have decided not to stick to the convert-test-fix methodology, however, you
will have to manually remove all trace statements from the .NET project. You can
easily do it with a global Find and Replace command, or you can define a Visual
Studio macro that does the job for you. The following text describes how to create
such a macro.
- Launch Visual Studio and select the Macro IDE command from the Macros submenu of
the Tools menu. This action brings you to the Microsoft Visual Studio Macros environment.
- Select the MyMacros node in the Project Explorer window, then select the Add Module
command from the Project menu.
- Name the new module appropriately – for example, “TraceMethods”
in the dialog box that appears, then click on the Add button to create the module.
This is what you should see now inside the Macro IDE:
Enter the following code inside the module:
Public Module TraceMethods
Sub RemoveTraceStatements()
Dim pattern As String = "([ \t]*Dim trace_ As VB6MethodTrace.+?\r\n(\r\n)?)" _
& "|([ \t]*AppTrace\.(ExitMethod|Init|Trace|Enabled)\(.+?(:|\r\n))" _
& "|([ \t]*trace_\.Trace\(.+?(:|\r\n))" _
& "|(Public AppTrace As New VB6AppTrace\r\n(\r\n)?)"
Dim reTrace As New System.Text.RegularExpressions.Regex(pattern, _
System.Text.RegularExpressions.RegexOptions.IgnoreCase)
For Each prj As EnvDTE.Project In DTE.Solution.Projects
For Each prjItem As EnvDTE.ProjectItem In prj.ProjectItems
For i As Short = 0 To prjItem.FileCount - 1
Try
Dim filename As String = prjItem.FileNames(i)
If filename.Contains("\My Project\") Then Continue For
Dim text As String = System.IO.File.ReadAllText(filename)
text = reTrace.Replace(text, "")
System.IO.File.WriteAllText(filename, text)
Catch ex As System.Exception
End Try
Next
Next
Next
End Sub
End Module
You can now save the macro you’ve just created and return the Visual Studio
(use the Close and Return command from the File menu).
From inside Visual Studio, select the Macro Explorer command from the Macros submenu
of the Tools menu, which brings up the Macro explorer window.
You can finally right-click on the RemoveTraceStatements element and select the
“Run” command to remove all trace statements. The following dialog might
appear: if so, just click “Yes” to reload all modified files.
IMPORTANT: this action is destructive and overwrites all the files
in the current VB.NET solution. It is therefore essential that you run the macro
only after creating a backup of the solution, in case the remove operation mistakenly
deletes vital portions of your code.
Conclusions
The Trace-Match methodology is a powerful tool that allows VB Migration Partner
users to quickly reach functional equivalence and to obtain “objective”
evidence that such equivalence has been reached.
By adding additional trace statements, developers can easily create a set of test
cases that can run unattended. For example, such additional statements can trace
the contents of user interface elements – e.g. the text inside a TextBox,
or the selected item in a ListBox control – so that no human intervention
is necessary to confirm that the converted project delivers correct results.
Code Architects is the only vendor that offers Trace-Match or a comparable tracing
methodology. Not only do we allow you to prove that your code works and
behaves like the original VB6 code, we even provide you with the tools that quickly
insert and remove all the trace statements on your behalf.