NDepend and CruiseControl.NET

NDepend is one of the best code analysis tools out there for determining code quality! It does so with some advanced metrics and I can say I still don't understand all of it, but we take advantage of the areas we do understand! It will help point you to possible problems with code during code reviews and it has some nice images to help visualize what's going on in your code.  You can even hook it up to CruiseControl.NET (CC.NET) and see reports for NDepend per build. We are going to talk about how to do that as well as displaying images.

One of the most interesting things you have to do to get images working for NDepend on CruiseControl.NET is to apply an image handler.  This is based on Robin Curry's original method (and still the one that is referred to by NDepend) for getting images into CC.NET build reports. It does further abstract the location of the images to the build's known location instead of hard coding it into the handler. Don't worry if you don't understand that, you will by the time we are done.

Get Revision Information Merged In

One thing we won't get in our build log on CC.NET is our revision number. So we need to make sure that information gets merged in.

In our builds, we set up the revision number:

<target name="generate_build_info_file">
  <echo message="Generating XML file with some information to possibly get merged in at ${files.build_info}."/>
  <echo file="${files.build_info}" append="false" failonerror="false">
    <![CDATA[
    <revision>${version.revision}</revision>
    ]]>
  </echo>
</target>

This creates a file named _buildinfo.xml.

image

The contents of this file are:

      <revision>5</revision>

Then we import that on every project, along with NDepend xml. This is done in the ccnet.config file for each project. Below nearly everything else has been removed for clarity. This also takes advantage of Preprocessors.

<project name="$(projectName)">
...
<publishers> <merge> <files> <file>$(working_directory)\$(projectName)\build_output\build_artifacts\*.xml</file> <file>$(working_directory)\$(projectName)\build_output\build_artifacts\mbunit\*-results.xml</file> <file>$(working_directory)\$(projectName)\build_output\build_artifacts\nunit\*-results.xml</file> <file>$(working_directory)\$(projectName)\build_output\build_artifacts\ncover\*-results.xml</file> <file>$(working_directory)\$(projectName)\build_output\build_artifacts\ndepend\*.xml</file> </files> </merge>
....
<xmllogger/> </publishers> </project>

This will merge that file into each build log. That means that <revision> is now available for our use.

XSL File for NDepend Reports

We have some good information available in our build logs file after a build that we can use for our needs:

<integrationProperties>
  <CCNetArtifactDirectory>c:\CodeDrop\Bombali</CCNetArtifactDirectory>
  ...
  <CCNetLabel>16</CCNetLabel>
  <CCNetNumericLabel>16</CCNetNumericLabel>
  <CCNetProject>Bombali</CCNetProject>
  <CCNetProjectUrl>http://robz.homedns.org:8081/ccnet</CCNetProjectUrl>
  ...
</integrationProperties>

Remember that _buildinfo.xml file we merged in?

We also get this near the top of the file:

</integrationProperties>
 <build date="2009-05-31 14:58:31" buildtime="00:00:00" buildcondition="IfModificationExists">
     
<revision>108</revision>
     

Now we can use that information so that we can refer to images in the ndependreport-ccnet.v2.xsl (comes with NDepend).

First we need to set up our xsl variables at the top of the file.

<xsl:variable name="label" select="//CCNetNumericLabel/text()" />
<xsl:variable name="revision_number" select="//revision/text()" />
<xsl:variable name="project_directory" select="//CCNetArtifactDirectory/text()" />
<xsl:variable name="project_url" select="//CCNetProjectUrl/text()" />

Then there are three locations where the NDepend XSL refers to images.  We will update all three of them keeping the name of the image.

<img>
  <xsl:attribute name="src">
    <xsl:copy-of select="$project_url" />/image.ashx?project=<xsl:copy-of select="$project_directory" />&amp;label=<xsl:copy-of select="$label" />&amp;revision=<xsl:copy-of select="$revision_number" />&amp;name=AbstractnessVSInstability.png
  </xsl:attribute>
</img>

Here is a picture to illustrate (that line is very long):

image 

The important thing to note is that we are passing query string information to image.ashx so that it can find and include the NDepend images.

We need to include that file with CruiseControl.NET. That goes in the XSL directory of the webdashboard. That is usually in  C:\Program Files\CruiseControl.NET\webdashboard\xsl.

image

Make sure your dashboard.config file knows to include NDepend Reports. This is usually in C:\Program Files\CruiseControl.NET\webdashboard. Mine is in a location where I can keep it in source control, but the relative path works for both. You can choose to remove ..\webdashboard if you keep the dashboard.config file in that location.

<buildPlugins>
    <buildReportBuildPlugin>
        <xslFileNames>
            ...
            <xslFile>..\webdashboard\xsl\ndependreport-ccnet.v2.xsl</xslFile>
            ...
        </xslFileNames>
    </buildReportBuildPlugin>
    <buildLogBuildPlugin />
    ...
    <xslReportBuildPlugin description="NDepend Report" actionName="NDependBuildReport" xslFileName="..\webdashboard\xsl\ndependreport-ccnet.v2.xsl" />
    ...
</buildPlugins>

The Image Handler

The image handler (image.ashx) from Robin must be added to the webdashboard folder of CC.NET's install location. I tried not to change Robin's handler much, but I made a few tweaks to suit my needs (in bold):

<%@ webhandler language="C#" class="ImageHandler" %>
using System;
using System.Configuration;
using System.Data; 
using System.Data.SqlClient; 
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Net;
using System.Web;
using System.Web.Caching;
 
 
public class ImageHandler : IHttpHandler
{
  public bool IsReusable  { get { return true; }  }
 
  private string _imagePath = @"{0}\b{1}-r{2}\build_artifacts\ndepend\";
 
  /// <summary>
  /// Processes the request.
  /// </summary>
  /// <param name="ctx">CTX.</param>
  public void ProcessRequest(HttpContext ctx)
  {
    try
    {
      string name = ctx.Request.QueryString["name"];
      string project = ctx.Request.QueryString["project"];
      string label = ctx.Request.QueryString["label"];
      string revision = ctx.Request.QueryString["revision"];
      string cacheKey = string.Format("{0}|{1}|{2}|{3}", name, project, label, revision);
 
      if ( name != null && name.Length > 0)
      {
        Byte[] imageBytes = null;
 
        // Check if the cache contains the image.
        object cachedImageBytes = ctx.Cache.Get(cacheKey);
 
        // Use cache if possible...
        if ( cachedImageBytes != null )
        {
          imageBytes = cachedImageBytes as byte [];
        }
        else // Get the image from the project/build directory.
        {   
          // Determine the base path from config file if provided.
          string imagePath = ConfigurationSettings.AppSettings["imagePath"];
          if ( imagePath == null || imagePath.Length == 0 )
          {
            // If not provided, default to:
            imagePath = _imagePath;
          }
 
          // Replace tokens in the provided path with the project and label.
          imagePath = string.Format(imagePath, project, label, revision);
 
          // If the path is relative, combine with the current base directory.
          if ( !Path.IsPathRooted(imagePath) )
          {
            imagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, imagePath);          
          }
 
 
          try
          { // Get the image stream from the provided path.
            using ( FileStream fs = new FileStream(Path.Combine(imagePath, name), FileMode.Open, FileAccess.Read) )
            {
              using(Image inputImage = Image.FromStream(fs))
              {
                using(Image outputImage = new Bitmap(inputImage))
                { 
                  using(MemoryStream stream = new MemoryStream())
                  {
                    outputImage.Save(stream, ImageFormat.Jpeg);
                    imageBytes = stream.GetBuffer();
                  }
 
                  ctx.Cache.Add(cacheKey, imageBytes, null,
                    DateTime.MaxValue, new TimeSpan(2, 0, 0),
                    CacheItemPriority.Normal, null);   
                }
              }
            }
          }
          catch ( Exception )
          {
            throw;
          }
        }
 
        ctx.Response.Cache.SetCacheability(HttpCacheability.Public);
        ctx.Response.ContentType = "image/jpg";
        ctx.Response.BufferOutput = false;
        ctx.Response.OutputStream.Write(imageBytes, 0, imageBytes.Length);
      }  
    }
    catch ( Exception )
    {
      throw;
    }
    finally
    {
      ctx.Response.End();    
    }
  }
}

Save that file as image.ashx and drop it into the correct folder. Most likely that is C:\Program Files\CruiseControl.NET\webdashboard.

image

Reports: The Results!

This is what we are passing image handler when we look at a report.

<img src="http://robz.homedns.org:8081/ccnet/image.ashx?project=c:\CodeDrop\Bombali&amp;label=17&amp;revision=108&amp;name=VisualNDependView.png" />

It can take that information and find our images:

image

Then we sit back and watch the image handler do it's thing and show us images.

image

image

Nice. By the way, UppercuT does all of this and has instructions and files that you can use that are already ready to take advantage of NDepend images in CC.NET. UppercuT also has a detailed explanation of getting set up with CC.NET.

 

kick it on DotNetKicks.com

Print | posted @ Tuesday, June 2, 2009 6:29 AM

Comments on this entry:

Gravatar # re: NDepend and CruiseControl.NET
by Robz at 6/2/2009 8:25 AM

It looks like 1.4.4 implements an NDepend task.
http://confluence.public.thoughtworks.org/display/CCNET/CCNet+1.4.4+Release+Notes
Comments have been closed on this topic.