Tuesday, November 14, 2006
I've gotten several email requests about using ATAPI from ASP.NET -- people have had trouble getting it to function properly so I figured I'd post an example of how it works. First, I'd recommend using ATAPI and not ITAPI3 - only because the latter pulls in COM objects which always makes things much more complex. The key thing to remember is where to hook up your events. Don't hook events inside your ASP.NET page-derived classes - that will keep the pages alive and cause memory leaks. Instead, use static methods (as presented here) or hook the events in global.asax or some other global shared class. I created a simple dialer in ASP.NET 2.0 through the following steps:
  1. Create an ASP.NET website
  2. Add a reference to ATAPI.DLL
  3. Add a Global Application Class (global.asax)
  4. Create the TapiManager class in your global application class
  5. Utilize the TapiManager from your pages
As an example, here is my global.asax class - I chose to store the TapiManager class into the Application property bag, but you could just make it a global as well if you prefer that approach.
void Application_Start(object sender, EventArgs e) 
{
    // Code that runs on application startup
    JulMar.Atapi.TapiManager tapiManager = new JulMar.Atapi.TapiManager("TestWebApp");
    if (!tapiManager.Initialize())
        System.Diagnostics.EventLog.WriteEntry("Application", "TapiManager failed to initialize");
    else
    {
        Application["tapi"] = tapiManager;
    }
}
    
void Application_End(object sender, EventArgs e) 
{
    //  Code that runs on application shutdown
    JulMar.Atapi.TapiManager tapiManager = (JulMar.Atapi.TapiManager)Application["tapi"];
    if (tapiManager != null)
        tapiManager.Shutdown();
}
Next, I added some markup to my default.aspx file to give me some server-side controls to manipulate TAPI with:
<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h1>Sample TAPI Dialer</h1>
        <asp:Label runat="server" Text="Select Line:" />&nbsp;&nbsp;
        <asp:DropDownList ID="lineList" runat="server" />
        <br />
        <asp:Label runat="server" Text="Number to dial:" />&nbsp;&nbsp;
        <asp:TextBox runat="server" Width="100" ID="number" />&nbsp;&nbsp;
        <asp:Button runat="server" ID="dial" OnClick="DialNumber" Text=" Dial " />
        <asp:Button runat="server" ID="refresh" Text=" Refresh " />
        <br />
        <br />
        <asp:ListBox runat="server" ID="events" Width="400" Height="300" EnableViewState="false" />
    </div>
    </form>
</body>
</html>
Finally, I added the code-behind to actually do the dialing. As part of this, I hook the appropriate events using static methods - not instance methods so I don't keep the page alive longer than a single request. I also store all my TAPI events in a global string collection so that when the page is refreshed, I can display the current set of events.
using System;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Specialized;
using JulMar.Atapi;

public partial class _Default : System.Web.UI.Page { static StringCollection data = new StringCollection();
protected void Page_Load(object sender, EventArgs e) { TapiManager tapiManager = (TapiManager)Application["tapi"]; if (!Page.IsPostBack) { if (tapiManager != null) lineList.DataSource = tapiManager.Lines; }
events.DataSource = data; DataBind(); }
protected void DialNumber(object sender, EventArgs e) { TapiManager tapiManager = (TapiManager)Application["tapi"]; string lineName = lineList.SelectedValue;
TapiLine line = tapiManager.GetLineByName(lineName, true); if (line != null) { if (!line.IsOpen) { try { line.Open(MediaModes.InteractiveVoice); } catch { line.Open(MediaModes.DataModem); }
line.NewCall += new EventHandler<NewCallEventArgs>(line_NewCall); line.CallInfoChanged += new EventHandler<CallInfoChangeEventArgs>(line_CallInfoChanged); line.CallStateChanged += new EventHandler<CallStateEventArgs>(line_CallStateChanged); }
if (number.Text.Length > 0) { TapiCall call = line.MakeCall(number.Text); data.Add(string.Format("Created call: {0}", call)); } } }
static void line_NewCall(object sender, NewCallEventArgs e) { data.Add(string.Format("New call: {0}, {1}", e.Call, e.Privilege)); }
static void line_CallStateChanged(object sender, CallStateEventArgs e) { data.Add(string.Format("CallState: {0} is now {1}", e.Call.ToString(), e.CallState)); }
static void line_CallInfoChanged(object sender, CallInfoChangeEventArgs e) { data.Add(string.Format("CallInfo: {0} {1}", e.Call.ToString(), e.Change)); } }
This deploys and executes properly on Windows 2003 Server and IIS6. I did not try it under XP or IIS5, although I see no reason why it would not work as shown. Hopefully this will help someone out there trying to use TAPI from within a website!
posted on 11/14/2006 10:01:25 AM (Central Standard Time, UTC-06:00)  #   
 Friday, September 01, 2006
I was preparing a sample memory leak application for an Advanced C# class at Microsoft this past week and debugging through it with SOS.DLL ("Son of Strike"). My prepared application was an ASP.NET application that would leak memory by holding references to the page objects after they had completed their work. I did this by having the page hook up an event handler to a global event and then never remove the handler.

This, of course, is bad form because the System.Web.UI.Page object is intended to be a transient object - it goes away at the end of the request - in production code, I would really bind the event to a handler in global.asax instead. But as I said, this was a sample.

So, I was debugging through it prior to the class just to make sure it exhibited the behavior I was looking for. I run the web page a few times, noted my working set going up and never coming back down. Next, I used ADPLUS.VBS to take a hang dump of the process. I then loaded this dump up into WinDBG and started poking around. First, I looked at the heap and sure enough I saw a bunch of page objects:
0:000> .load sos
0:000> !DumpHeap -stat
total 36955 objects
Statistics:
      MT    Count    TotalSize Class Name
7b4ecd7c        1           12 System.Windows.Forms.ButtonInternal.ButtonPopupAdapter
7b481f00        1           12 System.Windows.Forms.LinkLabel+LinkComparer
7b475ca8        1           12 System.Windows.Forms.FormCollection
7b474f8c        1           12 System.Windows.Forms.Layout.DefaultLayout
7b4749e0        1           12 System.Windows.Forms.ClientUtils+WeakRefCollection
7b473ca8        1           12 System.Windows.Forms.Layout.ArrangedElementCollection
7a755834        1           12 System.Diagnostics.PerformanceCounterCategoryType
7a753394        1           12 System.Diagnostics.TraceOptions
7a71a710        1           12 System.Net.TimeoutValidator
.......
00166030      891       169744      Free
054d24d4     3128       187680 System.Web.UI.LiteralControl
0548cbd4      519       197220 ASP.default_aspx
791242ec     1545       297960 System.Collections.Hashtable+bucket[]
79124670     1185      1090500 System.Char[]
79124228    11961      1279380 System.Object[]
790fa3e0    19149      1561392 System.String
Total 110069 objects
So, next I used DumpHeap to just look at this specific type by giving it a metadata token:
0:000> !DumpHeap -mt 0548cbd4        
 Address       MT     Size
.....     
01854ff0 0548cbd4      380     
01860130 0548cbd4      380     
0186b2b4 0548cbd4      380     
018773f8 0548cbd4      380     
01882538 0548cbd4      380     
0188d6bc 0548cbd4      380     
01898840 0548cbd4      380     
018a39c4 0548cbd4      380     
018aeb48 0548cbd4      380     
total 519 objects
Statistics:
      MT    Count    TotalSize Class Name
0548cbd4      519       197220 ASP.default_aspx
Total 519 objects
I then used the very cool GCRoot command to determine why a page instance was still rooted and therefore not collectable.

Note: it appears that GCRoot doesn't work well inside VS.NET 2005 - apparently the SOS debugging extension is using some debugger API which isn't fully supported in VS.NET, so you need to familiarize yourself with WinDBG to do this.


0:000> !gcroot 018aeb48 
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 3a8
Scan Thread 2 OSTHread e8
Scan Thread 3 OSTHread 1a8
Scan Thread 6 OSTHread 7d4
Scan Thread 7 OSTHread 2b4
Scan Thread 8 OSTHread fdc
Scan Thread 9 OSTHread eac
DOMAIN(001E5E08):HANDLE(Pinned):12312f0:Root:0226c498(System.Object[])->
018af940(System.EventHandler)->
0186c0ac(System.Object[])->
018af920(System.EventHandler)->
018aeb48(ASP.default_aspx)
With this output, I can tell that my default_aspx object is being kept alive through an EventHandler as I expected. The interesting thing about this output is I cannot tell which event handler is keeping it alive - i.e. there is nothing in this root list that points to a specific object holding it other than a object[]. That essentially means this is a static event and there isn't an actual object around on the heap for it. This just makes the debugging exercise more interesting - after all if it were an instance event then I would see the class name at the top of the root list and we could stop right here.

So, my next step is to dump the event handler to try to identify what method it is wrapping - I could then search the code for this method and find out where it is being bound. I dump out the EventHandler object:
0:000> !do 018af920
Name: System.EventHandler
MethodTable: 7910d61c
EEClass: 790c3a7c
Size: 32(0x20) bytes
 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
790f9c18  40000f9        4        System.Object  0 instance 018aeb48 _target
79109208  40000fa        8 ...ection.MethodBase  0 instance 00000000 _methodBase
790fe160  40000fb        c        System.IntPtr  0 instance 88962888 _methodPtr
790fe160  40000fc       10        System.IntPtr  0 instance        0 _methodPtrAux
790f9c18  4000106       14        System.Object  0 instance 00000000 _invocationList
790fe160  4000107       18        System.IntPtr  0 instance        0 _invocationCount
From this I get the internals of the EventHandler object. It gives me some specific information:
  • _target is the object the delegate is holding onto - my default_aspx in this case. The value better match up with my original object!
  • _methodBase is used for dynamic code and is null here.
  • _methodPtr is the method associated with the instance, or possibly a dynamically generated thunk for static methods. This is what I'm interested in.
  • _methodPtrAux is used for static methods; this holds the method descriptor and the _methodPtr is a dynamically generated block of code to remove the this reference. Noting that this is null, I can infer that this is an instance method bound to the delegate.
  • _invocationList and _invocationCount are used for Multicast Delegates -- these are both zero indicating that this is a single delegate and there is no chain to follow.

My next step is to try to get a valid method name from the _methodPtr so I convert that to hex and pass it into IP2MD.
0:000> !ip2md 0n88962888 
Failed to request MethodData, not in JIT code range
This is a pretty common error and simply means that the method may not have been JITted yet, or may be a dynamic block of code which never went through the JIT compiler. In .NET 1.1 we could start dumping method descriptors and trying to match up the address (DumpClass -md) for this class and it's base class.

However, under .NET 2.0 this rarely works anymore - the address doesn't appear to ever match up to a valid descriptor. However, it clearly is part of the managed heap due to it's address. So, failing to locate this address, I tried disassembling the code:
0:000> !u 0n88962888 
Unmanaged code
054d7748 e862289b74      call    mscorwks!LogHelp_TerminateOnAssert+0x3f5f (79e89faf)
054d774d 5e              pop     esi
054d774e cc              int     3
054d774f cc              int     3
054d7750 38c8            cmp     al,cl
054d7752 48              dec     eax
054d7753 05a0774d05      add     eax,54D77A0h
054d7758 0100            add     dword ptr [eax],eax
054d775a 0011            add     byte ptr [ecx],dl
054d775c 0000            add     byte ptr [eax],al
Humph. This doesn't even look like valid code to me.. this looks like random data, so I dumped it out:
0:000> dd 0n88962888 
054d7748  9b2862e8 cccc5e74 0548c838 054d77a0
054d7758  11000001 90000000 054d77a0 11000002
054d7768  90000004 00000000 054d77a0 00000000
The third and fourth DWORD look interesting because they appear to fall in the managed heap as well -- so I began to dump them out trying to figure out what they were. I found that the first value is a method descriptor:
0:000> !dumpmd 0548c838 
Method Name: _Default.OnDatabaseHasChanged(System.Object, System.EventArgs)
Class: 054ab574
MethodTable: 0548c86c
mdToken: 06000004
Module: 0548c35c
IsJitted: no
m_CodeOrIL: ffffffff
It is the real method bound to the delegate instance. The other DWORD appears to be an metadata reference to the event owner itself:
0:000> !dumpmt 054d77a0
EEClass: 0551940c
Module: 048ac9ec
Name: DatabaseMonitor
mdToken: 02000002  (C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\leakypage\2a399ab5\b1e04c63\App_Code.onwg1zqj.dll)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 8
From here I can see the module that this code is defined in (the dynamically generated App_Code directory) and the name (DatabaseMonitor). This gives me enough information to stop here and begin looking at the code itself - specifically where default.aspx binds it's OnDatabaseHasChanged method to the DatabaseMonitor static class defined somewhere in the App_Code directory. If I didn't have the source code available, I could locate the module and then save it out to a file through the savemodule command:
0:000> lm m App_Code_onwg1zqj
start    end        module name
04de0000 04de8000   App_Code_onwg1zqj C (no symbols)           
0:000> !savemodule 04de0000 c:\appcode.dll
3 sections in file
section 0 - VA=2000, VASize=504, FileAddr=200, FileSize=600
section 1 - VA=4000, VASize=2c8, FileAddr=800, FileSize=400
section 2 - VA=6000, VASize=c, FileAddr=c00, FileSize=200
I could then ILDasm or Reflector the generated assembly and look for my bug from that.

I love SOS and WinDBG.. if only they could match up managed source code to the image I'd abandon VS.NET as a debugger altogether..
posted on 9/1/2006 12:18:41 PM (Central Standard Time, UTC-06:00)  #   
A DM colleague of mine, Neils Berglund noted his required applications under OS X and asked for feedback on what everyone else is using (assuming you are on OS X anyway).

The original question was what RSS reader are you using.. my preference is NetNewsWire as I was used to the Newsgator line of products when using Windows all the time so it made an easy transition for me. I also use MarsEdit to update posts on this blog -- it's not WYSIWYG but it keeps my HTML skills (if I ever had any) from drying up.

So, adding to Neils list, I also use --
  • Final Cut Express - for editing movies, I found iMovie to be too limiting.
  • CHMOX - to open and read .CHM (Windows Help) files.
  • DivX codecs - to play my 300+ .divx movies.
  • Smultron - a decent, quick color-coded text editor
  • Virtue Desktops - an awesome window manager.
  • Remote Buddy - allows the Apple Remote to be used to control other applications such as Power Point.
  • Microsoft Office - I just can't live without Word/Excel/Power Point. I know Apple has iWork, but it just isn't quite the same.
  • Flip4Mac - Codecs for .WMV (Windows Media), essential in a 99.999% Windows dominated world.
  • Coverflow - this isn't critical, but it's a fun app to show off - it let's you view your music through the album covers in a cool, flip-3d style fashion.

I'm also trying out a .mac membership for backup purposes. Not sure I'll stay with that, but it is automatic and seems to work even when I travel.

So, what things do you think are necessary applications under OS X?
posted on 9/1/2006 10:15:50 AM (Central Standard Time, UTC-06:00)  #   
 Monday, August 28, 2006
One question I get a lot when teaching the VSTS course is "How can I move tasks or bugs from our existing system into TFS?"

The answer is quite simple: write a program that utilizes the Team Systems Object Model.

The Object Model is documented partially in the VSIP SDK -- if you intend to work with it, I highly recommend you go and grab that SDK from Microsoft, but here is a simple example of creating a bug in the first Team Project available on the server named TFSServer (you can imagine the rest which uses ADO.NET to pull your existing tracking tickets from whatever system you have):

   1:  using System;
   2:  using System.Collections;
   3:  using System.Collections.Generic;
   4:  using System.Text;
   5:  using Microsoft.TeamFoundation.Client;
   6:  using Microsoft.TeamFoundation.WorkItemTracking.Client;
   7:   
   8:  namespace EnterWorkItems
   9:  {
  10:      class Program
  11:      {
  12:          static void Main(string[] args)
  13:          {
  14:              TeamFoundationServer tfs = new TeamFoundationServer("TFSServer");
  15:              WorkItemStore wis = (WorkItemStore) tfs.GetService(typeof(WorkItemStore));
  16:   
  17:              Project teamProject = wis.Projects[0];
  18:              foreach (WorkItemType wit in teamProject.WorkItemTypes)
  19:                  Console.WriteLine(wit.Name);
  20:   
  21:              WorkItemCollection wic = wis.Query("Project='Test' AND Type='Bug'");
  22:              foreach (WorkItem wiEntry in wic)
  23:              {
  24:              }
  25:   
  26:              WorkItemType witBug = teamProject.WorkItemTypes["Bug"];
  27:              if (witBug != null)
  28:              {
  29:                  Console.WriteLine("Adding new bug to Team Project {0}", teamProject.Name);
  30:   
  31:                  WorkItem wi = new WorkItem(witBug);
  32:                  wi.Description = "This is a sample bug which was added through the object model";
  33:                  wi.Reason = "New";
  34:                  wi.Title = "You have Bugs! [Ding]";
  35:   
  36:                  wi.Save();
  37:                  Console.WriteLine("Added Work Item # {0} created by {1}", wi.Id, wi.CreatedBy);
  38:              }
  39:          }
  40:      }
  41:  }


Breaking this down a bit, the first main piece of code is the connection to the Team Server itself. This is accomplished on line 14 by creating a new TeamFoundationServer object. An optional constructor allows for credentials to be provided, otherwise it logs on as the current principle.

Next, we retrieve the WorkItemStore. Almost everything in the object model is accessed through the IServiceProvider interface which provides a GetService method where you pass in the System.Type object you want to work with. This is a nice versioning technique that is utilized in many other Microsoft technologies as well.

With the WorkItemStore, you can then query work item type definitions, one of which is necessary to create a WorkItem. You fill in the details for the Work Item you want to create and call the Save method (line 36) to commit the changes to the TFS store. The WorkItem id will then be valid and could be added to the existing bug tracking system as a forward link to the new information if you wanted.
posted on 8/28/2006 5:45:13 AM (Central Standard Time, UTC-06:00)  #   
 Monday, July 17, 2006
I was having lunch with an associate a while back and he mentioned a need to move a work item from one team project to another. While there isn't any direct support for this from the Team Explorer interface, I figured it couldn't be too hard to manipulate the underlying database and achieve the results - it's just SQL Server right? I hadn't actually looked at the schema mind you, I'm an optimist.

It wasn't quite as simple as I thought - it felt like the database had been designed by the security group and intentionally obscured for privacy. The TFS database is fairly generic (so that it can add arbitrary items into the schema for custom project types) and has been somewhat secured to protect the code itself - the stored procedures are encrypted and SQL Profiler doesn't give much information on what's happening.

So, I spent a week or so looking at the schema for the Work Item system and a lot of trial and error. End result is that I got a simple application to move work items around. This application does a couple of other things as well - lists projects, active web services, etc. I was mostly playing with the schema and trying to figure things out. It's just a console application but it does the job.

Here's the code, use at your own risk (a.k.a. if it screws up your system, I can't help you). The source for the simple test project is included so you can see what was done and incorporate it into your own code base if you like.

The program is fairly easy to use - a binary is included with the release. Issuing the command with no parameters generates a simple "Help" page --

C:\Work\TfsCmd>tfscmd
TfsCmd [command] [/t tfsserver] [/u user] [/p password] [params]
ListProjects - Lists the active projects on the TF server.
TfsCmd ListProjects
ListServices - Displays the web services exposed by the TF server.
TfsCmd ListWebServices
WQL - Execute a WorkItem Query.
TfsCmd Wql [query]
MoveWorkItem - Moves a work item from one Team Project to another
TfsCmd MoveWorkItem [id] [ProjectName]

Executing a command will use your logged on user/password (domain credentials) unless you supply a /u and /p parameter. For example, running the ListProjects command generates something like:

C:\Work\TfsCmd>tfscmd ListProjects
Project Name: Test
Status: WellFormed
Guid: 8c5b175b-b0ef-46bb-9a9e-dde05bb145ac
Process Template: MSF for Agile Software Development - v4.0
Defined Properties: MSPROJ

Listing the web services just hits the underlying database to get the information:

C:\Work\TfsCmd>tfscmd ListServices
BuildStoreService = http://(local):8080/Build/v1.0/BuildStore.asmx
BuildControllerService = http://(local):8080/Build/v1.0/BuildController.
IBISEnablement = http://(local):8080/Build/v1.0/Integration.asmx
LinkingProviderService = http://(local):8080/Build/v1.0/Integration.asmx
IProjectMaintenance = http://(local):8080/Build/v1.0/Integration.asmx
PublishTestResultsBuildService = http://(local):8080/Build/v1.0/PublishT tsBuildService.asmx

Moving work items is fairly easy - just provide the work item id (the numeric identifier) and the new project name - this must match an existing project in your TFS system. The tool will attempt to match up the iteration to a value in the new project - if it cannot, it will default to the first iteration available.


C:\Work\TfsCmd>tfscmd MoveWorkItem 1 "Mark's New Project"
Work Item #1: "Create Project Definition" is currently located in project "Test Project (Iteration 0)"
Moving work item to "Mark's New Project (Iteration 0)"

If you have the TFS client open, then you should shut it down and reopen it before modifying the moved work item - the client appears to cache bits of information and I had some issues with moved items if I didn't clear the cache by closing the work item window and reopening it.

Have fun!
posted on 7/17/2006 12:05:53 PM (Central Standard Time, UTC-06:00)  #   
 Monday, June 19, 2006
Recording telephone conversations with TAPI
posted on 6/19/2006 9:41:45 AM (Central Standard Time, UTC-06:00)  #   
 Monday, May 01, 2006

A colleague of mine, Kev Jones, has posted some information on using a detached SQL Server database for driving VSTS unit tests which works great if you need a full blown SQL implementation.  However, if all you want is a simple data feed, you can also use an Excel file, and as it turns out, it's pretty easy to do.

Let's start by stealing Kev's sample, hopefully he won't mind -- say I have a starship class with a FirePhotonTorpedo method:

public class Enterprise
{
   private int torpedosLeft = 10;

   public int FirePhotonTorpedo(int count)
   {
      if (torpedosLeft < count)
         throw new ArgumentException("count");

      torpedosLeft -= count;
      // Instruct bridge officer to "Fire!"
      return torpedosLeft;
   }
}

The equivalent unit test might look something like:

[TestMethod]
public
void TestFirePhotonTorpedo()
{
   Enterprise target = new Enterprise();
   int torpedoCount = 5;
   int expected = 5;

   int actual = target.FirePhotonTorpedo(torpedoCount);
   Assert.AreEqual(expected, actual, "FirePhotonTorpedo did not return the expected value.");
}

This code just tests a specific case -- I would also need to write other unit tests for edge cases and exceptional cases.  I can do this several different ways I could do this:

1) Write a unit test for each specific case passing each value and testing the expected result.
2) Put all the tests into this one unit test function -- asserting each validation as we go.
3) Pull the input and expected out of a database and run them through the function.

This last step, as Kev details can be done from a SQL database - either one reachable by all the people who might run the unit tests, or as his blog shows from a .MDF file you check in with the project and then locally attach prior to running the unit tests.  Hit the link above for the details on that. 

So, to make an Excel data-driven test, the first step is to create an Excel document with columns for each of our pieces of data.  My sample Excel document has the following columns:

ID A unique number identifying the row so we can use the Random data driven test
torpedoCount The input for our unit test
expected The expected result coming out of the function
shouldFail Whether the function will throw an exception.

I can then enter a single test on each row of the given worksheet.  Here's a screen shot --

You can create multiple "tables" by adding additional sheets to the worksheet.  Each sheet can be named as appropriate; I'm naming this one "TorpedoData".

Now, I need to add the appropriate attribute to my test case to indicate that it should pull the data from a data source and run the method once per row found.  The key is that any ADO.NET data source can be used.  Here I will specify my Excel file which is named "UnitTests.xls" and indicate that the table itself is a particular sheet within the Excel document "TorpedoData":

[TestMethod]
[DeploymentItem("Tests.xls")] // Copies the file to the deployment directory
[DataSource(
"System.Data.OleDb", // The provider
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source='Tests.xls';Persist Security Info=False;Extended Properties='Excel 8.0'",
"TorpedoData$",      // The table name, in this case, the sheet name with a '$' appended.
DataAccessMethod.Sequential)] 

public
void TestFirePhotonTorpedo()
{

}

I also need to update the test case itself to use the data -- we do that through the TestContext.DataRow property that gives us access to the current row of data from our data source:

public void TestFirePhotonTorpedo()
{

   Enterprise target = new Enterprise();
  
int torpedoCount = Convert.ToInt32(TestContext.DataRow["torpedoCount"]);
   int expected = Convert.ToInt32(TestContext.DataRow["expected"]);
   bool shouldFail = (bool)TestContext.DataRow["shouldFail"];

   TestContext.WriteLine("Running test with TorpedoCount={0}, ExpectedCount={1}, ShouldFail={2}",
            torpedoCount, expected, shouldFail);

   try
   {
      int actual = target.FireTorpedos(torpedoCount);
      Assert.AreEqual(expected, actual, "FireTorpedos did not return the expected value.");
   }
   catch (Exception ex)
   {
      if (!shouldFail)
         Assert.Fail(string.Format("FireTorpedo threw exception {0}", ex.Message));
   }
}

So, here we will grab the data from the current row, converting it as necessary to the appropriate types, output a test line just to prove that we executed the method more than once and then run the test.  The test will compare the result with the expected database result and output a failure if they aren't the same.  If an exception is thrown, then the shouldFail must be true or that will be considered a failure.

This approach allows me to run through different scenarios very easily and I can just store the Excel worksheet right with my unit tests - make sure it's deployed to the target deployment directory (either through a DeploymentItem attribute, or through the test run configuration).  The size of my table here is about 14K, compared to the equivalent .MDF file which is over 2M for the same data.  If I didn't want to hardcode the filenames and such, I can also use an app.config file for the unit test and put the information there - just as Kev details.

Cool stuff indeed.

posted on 5/1/2006 10:55:40 AM (Central Standard Time, UTC-06:00)  #   
 Thursday, April 20, 2006

Minor, but breaking update for ATAPI.NET (version number has changed).  It was pointed out to me that the assembly wasn't very VB.NET friendly in that it didn't show any events at the TapiManager level and you couldn't use the UI to hook it all up.  That's fixed and all the line-level events are also exposed at the TapiManager level for those who want to use VB.NET with the 2.0 wrapper.

 

posted on 4/20/2006 8:01:49 AM (Central Standard Time, UTC-06:00)  #   
 Tuesday, April 04, 2006

Recently someone informed me that the TAPI wrappers (ITAPI3 and ATAPI) do not appear to function properly on the Win64 platform.  It throws an exception that says:

"Could not load file or assembly 'ITapi3, Version=1.0.0.3, Culture=neutral, PublicKeyToken=36377d9f6f1f4883' or one of its dependencies. An attempt was made to load a program with an incorrect format."

Now, as most know, .NET code compiles to an Intermediate Language (IL) which is a bytecode that is translated to the appropriate processor-specific instructions at runtime by the just-in-time (JIT) compiler.  The type of code that is generated is determined by the version of the CLR that is loaded.

When a .NET application starts, mscoree.dll is responsible for determining the proper version of the CLR to start for the process.  This is done by looking at the application manifest to see which version of .NET the application was compiled for (1.0, 1.1, or 2.0) and mscoree will suck in the appropriate assemblies and DLLs for that specific version, or in some cases, upgrade it silently to a newer version.

On Win64, the picture is a little more complex because we have to consider the platform as well.  There are actually two 2.x versions of .NET on Win64 - a 32-bit version (which is the same as the one running on Win32) and a 64-bit version which has been optimized for the 64-bit platform and generates 64-bit JITted code.  When an assembly starts on a Win64 platform, the mscoree.dll will look at not just the version, but also the platform flag which is coded into the manifest.  We can see this flag using ILDASM:

The .corflags above tells the loader that this particular assembly requires the 32-bit CLR, in other words, that we have a dependency on a 32-bit resource such as a COM object or platform DLL.  By default, the flag will be set to 0x0000001 (ILONLY) indicating no dependency (VS.NET refers to this as "AnyCpu" in the platform flag settings) and, on a Win64 machine, the assembly will be loaded under the 64-bit CLR.  With it set as above, on a Win64 machine it would be loaded into the 32-bit CLR.  For those who are interested in this aspect, there is a tool in the SDK (CORFLAGS.EXE) that will let you manipulate this flag and force an ILONLY assembly to be loaded as 32-bit.

VS.NET allows you to change the platform type on an assembly-by-assembly basis through the project settings, Build Tab:

When loading, this platform CLR determination appears to be, as with the CLR versioning, based solely on the assembly that starts the process.  The loader doesn't scan the dependencies and determine that one of the required assemblies is marked as '"x86" and will simply fail the process when that assembly eventually gets loaded.

So, if I have an assembly that requires 32-bit execution (such as ATAPI.NET or ITAPI3 that depend on the 32-bit TAPI subsystem and COM objects), but my starting assembly is marked as "AnyCpu", then the loader will start it under Win64 as a 64-bit process and when it tries to initialize ATAPI.NET or ITAPI3, it will fail the AppDomain with an exception.  Marking ATAPI.NET and ITAPI3 as "x86" (which they are by the way) won't help in this case -- you must mark your application as 32-bit.

This really is a bummer and, while I'm not surprise the CLR loader doesn't do it, I am surprised that VS.NET doesn't force the .EXE to be "x86" when it sees a dependency that requires it.

The wrappers do function under Win64, but only if the starting application has the platform target marked as "x86".

posted on 4/4/2006 9:39:12 AM (Central Standard Time, UTC-06:00)  #   
 Thursday, March 30, 2006

I have updated ITAPI3 to fix a couple of reported bugs -- the TE_FILETERMINAL event wasn't always being raised and the TCall.GenerateCustomTone didn't work properly.  Both of these issues are fixed in the latest drop.  Enjoy!

posted on 3/30/2006 5:16:09 PM (Central Standard Time, UTC-06:00)  #