Visual Studio Code Coverage reports with MSBuild

09/10/2008

Recently I had a project where I was using Microsoft Visual Studio 2008 Team System for development and CruiseControl.NET for doing continuous integration. I VisualStudioLogousually care about code coverage when running unit tests, so I decided to integrate the code profiling tool included in Visual Studio Team System as part of my build process, in order to produce a code coverage report with each build.

I figured that wouldn’t be too hard, all I had to do in my build script was to invoke Visual Studio’s test runner’s executable (usually found in C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\MSTest.exe) passing an option to enable code coverage profiling, and grab the output in a file that CruiseControl.NET would later use to produce the build report.

However, I quickly found out that Visual Studio’s test runner produces code coverage output in a binary proprietary format while CruiseControl.NET uses XML in order to generate its reports. Ouch!

Luckily, Microsoft distributes a .NET API that can be used to convert the content of code coverage files produced by Visual Studio into XML. Pheeew!

The library is contained in the Microsoft.VisualStudio.Coverage.Analysis.dll assembly, which can be found in the Visual Studio 2008 Team System installation folder (usually in C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies). So all I had to do was to add an extra step in the build process to invoke that library and do the conversion.

Since I am using MSBuild to run the build, I encapsulated the code in an MSBuild task which you can find over here at the MSDN Code Gallery. There isn’t really much to it, the actual conversion is easily done in a couple of lines of code:

// You need to specify the directory containing
// the binaries that have been profiled by MSTest
CoverageInfoManager.SymPath = symbolsDirPath;
CoverageInfoManager.ExePath = symbolsDirPath;

// The input file is the binary output produced by MSTest
CoverageInfo info = CoverageInfoManager.CreateInfoFromFile(inputFilePath);
CoverageDS dataSet = info.BuildDataSet(null);
dataSet.WriteXml(outputFilePath);

Then, the task can be invoked from the MSBuild script with:

<!-- Imports the task from the assembly -->
<UsingTask TaskName="ConvertVSCoverageToXml" AssemblyFile="CI.MSBuild.Tasks.dll" />

<!-- The values of the 'OutputPath' and 'TestConfigName' variables
     must be the same as the arguments passed to MSTest.exe
     with the /resultsfile and /runconfig options -->
<ConvertVSCoverageToXml 
    CoverageFiles="$(OutputPath)\$(TestConfigName)\In\$(ComputerName)\data.coverage"
    SymbolsDirectory="$(OutputPath)\$(TestConfigName)\Out"
    OutputDirectory="$(OutputPath)" />

This could easily be achieved in much the same way with an NAnt task, if that’s your build tool of choice.

Download Download VSCoverageToXml MSBuild Task

/Enrico

9 Responses to “Visual Studio Code Coverage reports with MSBuild”

  1. Rotte2 Says:

    Nice!

  2. megakemp Says:

    Thanks :-)

  3. Bryan Says:

    This is prefect, but I am having a problem in TFS 2008 SP1.

    With:
    CoverageFiles=”$(OutputPath)\$(TestConfigName)\In\$(ComputerName)\data.coverage”

    The:
    $(OutputPath)\$(TestConfigName)

    part seems to be resolving to nothing. I have found:
    $(TestResultsRoot)

    gets pretty close, but not quite there. There is a GUID that is used for the directory before the In directory. How do I get that as a MSBUILD property?


    • I haven’t used the TFS Build Agent to run the builds, so I’m not sure about what path the $(TestResultsRoot) variable gets set to at runtime.
      I manually wrote our MSBuild script which is run by CruiseControl.NET, and in that I explicitly set the output directory for the test results by invoking MSTest.exe with the /resultsfile:$(OutputPath)\mstest.log option.
      The name of the test output directory relative to the $(OutputPath) path can be then defined in the Test Run Configuration File (*.testrunconfig). You could set it to a fixed value, for example “TestWithCoverage”, which will make the result in the path “$(OutputPath)\TestWithCoverage\In\$(ComputerName)\data.coverage”.

      Hope this helps.

      /Enrico

      • Bryan Says:

        I edited the task to get the GUID off of the .trx file. It wasn’t too difficult, I just thought there would be a MSBUILD property that would get me there without that extra work. Thanks for your help.


      • I’m glad you found a solution :-)

      • Sergey Says:

        How did you do that? Could you please share your solution? Thank you.

  4. Bryan Says:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Resources;
    using CI.MSBuild.Tasks.Properties;
    using Microsoft.Build.Framework;
    using Microsoft.Build.Utilities;
    using Microsoft.VisualStudio.CodeCoverage;

    namespace CI.MSBuild.Tasks
    {
    ///
    /// Converts the coverage file generated by
    /// the Visual Studio Testing Tools (MSTest) into XML.
    ///
    public class ConvertVSCoverageToXml : Task
    {
    ///
    /// Initializes a new instance of the class.
    ///
    public ConvertVSCoverageToXml()
    : base(new ResourceManager(typeof(Resources)))
    {
    }

    ///
    /// Gets or sets the TestResultsDir
    ///
    public ITaskItem TestResultsDir { get; set; }

    ///
    /// Gets or sets the files containing
    /// Visual Studio code coverage data.
    ///
    [Required]
    public ITaskItem[] CoverageFiles { get; set; }

    ///
    /// Gets or sets the path to the directory
    /// containing the symbols of the intrumented binaries being covered.
    ///
    ///
    /// By default the symbols are expected to be in the
    /// same directory as the coverage file.
    ///
    public ITaskItem SymbolsDirectory { get; set; }

    ///
    /// Gets or sets the path to the directory
    /// where to store the converted files.
    ///
    ///
    /// By default the converted files will be saved
    /// in the directory where the build is run from.
    ///
    public ITaskItem OutputDirectory { get; set; }

    ///
    /// Gets the files generated from the conversion.
    ///
    [Output]
    public ITaskItem[] ConvertedFiles { get; private set; }

    ///
    /// Executes the task.
    ///
    ///
    /// true if the task successfully executed; otherwise, false.
    ///
    public override bool Execute()
    {
    this.Log.LogMessage(“Staring the Code Coverage to XML conversion…”);
    string[] files = Directory.GetFiles(this.TestResultsDir.ItemSpec, “*.trx”);
    string guid = null;
    string symbolsDir = null;
    if (files.Length >= 1)
    {
    guid = Path.GetFileNameWithoutExtension(files[0]);
    symbolsDir = String.Format(this.SymbolsDirectory.ItemSpec, guid);
    }

    List results = new List();

    foreach (ITaskItem file in CoverageFiles)
    {
    try
    {
    string sourceFile = String.Format(file.ItemSpec, guid);

    if (File.Exists(sourceFile))
    {
    Log.LogMessageFromResources(MessageImportance.Normal, “ConvertingVSCoverageFile”, sourceFile);

    CoverageInfoManager.SymPath = symbolsDir;
    CoverageInfoManager.ExePath = symbolsDir;
    CoverageInfo info = CoverageInfoManager.CreateInfoFromFile(sourceFile);
    CoverageDS dataSet = info.BuildDataSet(null);
    string outputFile = “CodeCoverage.xml”;

    // Unless an output dir is specified
    // the converted files will be stored in the same dir
    // as the source files, with the .XML extension
    if (OutputDirectory != null)
    {
    outputFile = Path.Combine(OutputDirectory.ItemSpec, Path.GetFileName(outputFile));
    }

    dataSet.WriteXml(outputFile);

    Log.LogMessageFromResources(MessageImportance.Normal, “WrittenXmlCoverageFile”, outputFile);

    ITaskItem item = new TaskItem(file);
    results.Add(item);
    }
    else
    {
    Log.LogMessageFromResources(MessageImportance.Normal, “SkippingNonExistentFile”, sourceFile);
    }
    }
    catch (Exception e)
    {
    Log.LogErrorFromException(e, true);
    }
    }

    ConvertedFiles = results.ToArray();

    return !Log.HasLoggedErrors;
    }
    }
    }

  5. nima Says:

    Hi could you give me a simple but complete example to how integrat code coverage to CC.net for VS solution
    tanks in advance


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 186 other followers