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
usually 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 VSCoverageToXml MSBuild Task
/Enrico




14/10/2008 at 20:43
Nice!
10/02/2009 at 02:13
Thanks :-)
09/07/2009 at 19:33
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?
10/07/2009 at 16:05
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
10/07/2009 at 17:43
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.
11/07/2009 at 13:32
I’m glad you found a solution :-)
22/12/2009 at 16:18
How did you do that? Could you please share your solution? Thank you.
28/12/2009 at 15:08
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;
}
}
}
20/01/2010 at 08:06
Hi could you give me a simple but complete example to how integrat code coverage to CC.net for VS solution
tanks in advance