Friday, December 08, 2006

There's been a discussion going on within DevelopMentor for a couple weeks regarding concurrent GC and when it really applies.

The idea behind the concurrent collector is to do as much of the GC while the UI thread continues to process UI stuff and then only interrupt the application threads when memory is being shuffled around and fixups are occurring. This provides for more responsive UI applications at the expense of slower collections and a higher memory utilization.

The instructors who are involved with .NET were discussing this because of a discrepancy in various reliable sources - First, Maoni Stephens, the ultimate authority on all things .NET GC, stated in her blog entries that concurrent GC was the default behavior for the workstation version of GC (see http://blogs.msdn.com/maoni for more information on this).

This is well known, and now well documented in various places. However, Jeff Richter seems to say in his book "CLR via C#" that concurrent collections occur only on multiprocessor machines. Several other authors back this up (notably Stephen Pratschner in "Customizing the .NET Framework Common Language Runtime" and Joe Duffy in his "Professional .NET Framework 2.0" book; both are excellent btw).

So, we had some differing opinions from people in the "know". Our Effective .NET and Essential .NET courses were written around Maoni's blogs and so the diagram we presented showed concurrent collections even on single-processor machines since she didn't explicitly say it required multiple processors to turn it on. One of the guys noticed this and said "Wait! That's wrong!"

We tried to actually see the concurrent collection in action but it turns out that it's actually quite difficult to get this to happen because concurrent collections only occur on Gen2, which for a well-written application shouldn't occur that often. To add to the complexity, the CLR attempts to optimize this behavior - just because it can do the GC concurrently, it might not.  Finally, the thread which is used for this collection is actually created and destroy as necessary so it doesn't exist most of the time.

So, to settle the argument, Jason Whittington and I spent some time spelunking into the CLR this past week to see if we could spot the concurrent collection in action. With a little WinDBG and some symbols, I think we've put the question to rest once and for all (at least for us) :-).  If you scan the GC symbols in mscorwks.dll, you'll find a treasure trove of information; one of the things that caught my eye was this:

0:000> x mscorwks!WKS::gcheap::*
79f8c5dd mscorwks!WKS::GCHeap::Relocate = <no type information>
7a0d09d9 mscorwks!WKS::GCHeap::ValidateObjectMember = <no type information>
7a088e2a mscorwks!WKS::GCHeap::IsConcurrentGCInProgress = <no type information>
Disassembling that method showed me that it checks a flag in memory --
0:000> u mscorwks!WKS::GCHeap::IsConcurrentGCInProgress 
mscorwks!WKS::GCHeap::IsConcurrentGCInProgress:
7a088e2a a1701b387a      mov     eax,dword ptr [mscorwks!WKS::gc_heap::settings+0x10 (7a381b70)]
7a088e2f c3              ret
I tried putting a breakpoint at the end of the GC process and dumping that flag out when it was a Gen2 collection (the breakpoint itself stolen shamelessly from Maoni's MSDN article in November 2006):
0:000> bp mscorwks!WKS::GCHeap::RestartEE "j 
(dwo(mscorwks!WKS::GCHeap::GcCondemnedGeneration)==2)
'dd mscorwks!WKS::gc_heap::settings+0x10 L1';'g'"
But I didn't get much success seeing it set to "1" - I suspect that it requires a more complex application than I was using to cause the CLR to decide a concurrent collection is worth the effort. So I decided this was a dead end and started looking to see where the GC process determines that concurrent collections are allowed at all. I figured GCHeap::Initialize sounded like a good spot --
0:000> u mscorwks!WKS::GCHeap::Initialize
mscorwks!WKS::GCHeap::Initialize:
79ed6924 a14412387a      mov     eax,dword ptr [mscorwks!g_pConfig (7a381244)]
79ed6929 8b4058          mov     eax,dword ptr [eax+58h]
.....
79ed697b 51              push    ecx
79ed697c e823040000      call    mscorwks!WKS::gc_heap::initialize_gc (79ed6da4)
79ed6981 85c0            test    eax,eax
Going into that function yielded what I was looking for --
0:000> u mscorwks!WKS::gc_heap::initialize_gc
mscorwks!WKS::gc_heap::initialize_gc:
79ed6da4 56              push    esi
79ed6da5 e80cf1ffff      call    mscorwks!WKS::write_watch_api_supported (79ed5eb6)
79ed6daa 33f6            xor     esi,esi
...
79ed6dc6 c70564ce387a01000000 mov dword ptr [mscorwks!WKS::gc_heap::gc_can_use_concurrent (7a38ce64)],1
Looking at that particular memory location, I tried various configurations to make sure I had the right spot. First, I did a simple console app with no config file and dumped out the location on a multi-processor box:
0:000> dd mscorwks!WKS::gc_heap::gc_can_use_concurrent L1
7a38ce64  <b>00000001b>
Next, I set the application up in server mode (also on a multi-processor box):
<xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <gcServer enabled="true" />
  </runtime>
</configuration>
0:000> dd mscorwks!WKS::gc_heap::gc_can_use_concurrent L1
7a38ce64  <b>00000000b>
Showing that it was now off.. next I tried a VM session which emulates a single processor:
0:000> dd mscorwks!WKS::gc_heap::gc_can_use_concurrent L1
7a38ce64  <b>00000001b>

It appears that the CLR is capable of doing concurrent collections on a single processor! My final test was to try turning it off altogether via a configuration switch:

<xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <gcConcurrent enabled="false" />
  </runtime>
</configuration>
0:000> dd mscorwks!WKS::gc_heap::gc_can_use_concurrent L1
7a38ce64  <b>00000000b>

As a final note on this, Jason asked Maoni directly and her response was that concurrent GC does not depend on the number of processors - just as we are seeing above. So, apparently several primary sources are actually incorrect on this behavior. It just goes to show how complex the overall GC process really is -- simple in concept, but boy there are a lot of moving parts!
posted on 12/8/2006 1:25:29 PM (Central Standard Time, UTC-06:00)  #   
I've updated ATAPI.NET in the samples page http://www.julmar.com/samples/atapinet.zip with several fixes and support for conferences, full transfers and phone support. I think almost everything is now wrapped - if you find any missing functions that you want/need ping me and I'll see what I can do.
posted on 12/8/2006 12:11:22 PM (Central Standard Time, UTC-06:00)  #   
 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)  #