Update in using StyleCop in TFS Team Build

I posted a while ago about trying to wire in the results from StyleCop into a Team Build, the problem I had was that I could not get the StyleCop violations into the build summary.

Well I still can’t, after much checking and asking around I was reliably informed that the build summary is not editable and there are no immediate plans for it to be in the future versions of TFS.

However, Martin Woodward, another Team System MVP, made the suggestion to add the violation information into the build information object. This would not allow the information to be seen in  the build summary in Visual Studio, but it would allow me to programmatically recover it from the IBuildInformation object inside my build wallboard application – which shows the current state of all our current CI Team Builds, it shows a scrolling list row as below

image

Where the key items are:

  • Big graphic showing Building, Success, Partial Success or Failure
  • The name of the build and the time it finished
  • CE – Compiler errors
  • CW – Compiler warnings
  • FW – FXCop warnings
  • SW – StyleCop violations
  • TP – Tests passed
  • TF – Test failed
  • and the rabbit shows if the build status is reported by a NazBazTag Build Bunny

So to do this I had to write an MSBuild Task, but the code fairly simple as Martin had suggested

//-----------------------------------------------------------------------
// <copyright file="StyleCopResultsMerge.cs" company="Black Marble">
//     Black Marble Copyright 2008
// </copyright>
//-----------------------------------------------------------------------
namespace BlackMarble.MSBuild.CodeQuality
{
    using System;
    using Microsoft.Build.Framework;
    using Microsoft.Build.Utilities;
    using Microsoft.TeamFoundation.Build.Client;
    using Microsoft.TeamFoundation.Client;
    
    /// <summary>
    /// Merges the Stylecop results into the build results for TFS
    /// </summary>
    public class StyleCopResultsMerge : Task
    {
        /// <summary>
        /// The tfs server to report to
        /// </summary>
        private TeamFoundationServer tfs;

        /// <summary>
        /// The build server doing the work
        /// </summary>
        private IBuildServer buildServer;

        /// <summary>
        /// The current build
        /// </summary>
        private IBuildDetail build;
                
        /// <summary>
        /// Gets or sets the Url of the Team Foundation Server.
        /// </summary>
        [Required]
        public string TeamFoundationServerUrl
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Uri of the Build for which this task is executing.
        /// </summary>
        [Required]
        public string BuildUri
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the number of stylecop violations found.
        /// </summary>
        [Required]
        public int Violations
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the number of files stylecop failed to parser.
        /// </summary>
        [Required]
        public int Failures
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the lazy init property that gives access to the TF Server specified by TeamFoundationServerUrl.
        /// </summary>
        protected TeamFoundationServer Tfs
        {
            get
            {
                if (this.tfs == null)
                {
                    if (String.IsNullOrEmpty(this.TeamFoundationServerUrl))
                    {
                        // Throw some exception.
                    }

                    this.tfs = TeamFoundationServerFactory.GetServer(this.TeamFoundationServerUrl);
                }

                return this.tfs;
            }
        }

        /// <summary>
        /// Gets the lazy init property that gives access to the BuildServer service of the TF Server.
        /// </summary>
        protected IBuildServer BuildServer
        {
            get
            {
                if (this.buildServer == null)
                {
                   this.buildServer = (IBuildServer)this.Tfs.GetService(typeof(IBuildServer));
                }

                return this.buildServer;
            }
        }

        /// <summary>
        /// Gets the lazy init property that gives access to the Build specified by BuildUri.
        /// </summary>
        protected IBuildDetail Build
        {
            get
            {
                if (this.build == null)
                {
                    this.build = (IBuildDetail)this.BuildServer.GetBuild(new Uri(this.BuildUri), null, QueryOptions.None);
                }

                return this.build;
            }
        }

        /// <summary>
        /// ITask implementation - Execute method.
        /// </summary>
        /// <returns>
        /// True if the task succeeded, false otherwise.
        /// </returns>
        public override bool Execute()
        {
            try
            {
                IBuildInformation info = this.Build.Information;

                Log.LogMessage("StyleCopResultsMerge for build {0} with {1} violations ", this.Build.Uri.ToString(), this.Violations.ToString());

                IBuildInformationNode infoNode = info.CreateNode();
                infoNode.Type = "org.stylecop";
                infoNode.Fields.Add("total-violations", this.Violations.ToString());
                info.Save();

                return true;
            }
            catch (Exception ex)
            {
                Log.LogError(ex.Message);
                return false;
            }
        }
    }
}


This can then wired into the build process I detailed in the older post and have repeated below with the new additions. The choice you have to make is if StyleCop violations will cause the build to fail or not – both are detailed below.



<!-- the imports needed -->
<Import Project="$(MSBuildExtensionsPath)\ExtensionPack\MSBuild.ExtensionPack.tasks"/>
<!-- this could be a task file if you wanted -->
<UsingTask AssemblyFile="$(BMTasksPath)BlackMarble.MSBuild.CodeQuality.StyleCopResultsMerge.dll" TaskName="BlackMarble.MSBuild.CodeQuality.StyleCopResultsMerge"/>

<!-- All the other Target go here -->

<Target Name="AfterCompile">

    <!-- Create a build step to say we are starting StyleCop -->
    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
        BuildUri="$(BuildUri)"
             Name="StyleCopStep"
              Message="StyleCop step is executing.">
      <Output TaskParameter="Id" PropertyName="StyleCopStep" />
    </BuildStep>

    <!-- Create a collection of files to scan, ** means and sub directories -->
    <CreateItem Include="$(SolutionRoot)\My Project\**\*.cs">
      <Output TaskParameter="Include" ItemName="StyleCopFiles"/>
    </CreateItem>

    <!-- Run the StyleCop MSBuild Extensions task using the setting file in the same directory as sln file and also stored in TFS -->
    <MSBuild.ExtensionPack.CodeQuality.StyleCop
        TaskAction="Scan"
        SourceFiles="@(StyleCopFiles)"
        ShowOutput="true"
        ForceFullAnalysis="true"
        CacheResults="false"
        logFile="$(DropLocation)\$(BuildNumber)\StyleCopLog.txt"
        SettingsFile="$(SolutionRoot)\My Project\Settings.StyleCop"
        ContinueOnError="false">
      <Output TaskParameter="Succeeded" PropertyName="AllPassed"/>
      <Output TaskParameter="ViolationCount" PropertyName="Violations"/>
      <Output TaskParameter="FailedFiles" ItemName="Failures"/>
    </MSBuild.ExtensionPack.CodeQuality.StyleCop>

    <!-- Run the new results merge task -->
    <BlackMarble.MSBuild.CodeQuality.StyleCopResultsMerge
      TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
      BuildUri="$(BuildUri)"
      Violations="$(Violations)"
      Failures ="0"
     />
 
    <!-- Put up a message in the build log to show results irrespective of what we do next -->
    <Message Text="StyleCop Succeeded: $(AllPassed), Violations: $(Violations)"/>

    <!-- FailedFile format is:
        <ItemGroup>
            <FailedFile Include="filename">
                <CheckId>SA Rule Number</CheckId>
                <RuleDescription>Rule Description</RuleDescription>
                <RuleName>Rule Name</RuleName>
                <LineNumber>Line the violation appears on</LineNumber>
                <Message>SA violation message</Message>
            </FailedFile>
        </ItemGroup>-->

    <Warning Text="%(Failures.Identity) - Failed on Line %(Failures.LineNumber). %(Failures.CheckId): %(Failures.Message)"/>

    <!-- The StyleCop task does not throw an error if the analysis failed, 
         so we need to check the return value and if we choose to treat errors as warnngs 
         we need to set the error state -->
    <Error Text="StyleCop analysis warnings occured" Condition="'$(AllPassed)' == 'False'"  />

    <!-- List out the issues, you only need this if we are not forcing the error above -->
    <!--<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
            BuildUri="$(BuildUri)"
            Message="%(Failures.Identity) - Failed on Line %(Failures.LineNumber). %(Failures.CheckId): %(Failures.Message)"/>-->

    <!-- Log the fact that we have finished the StyleCop build step, as we had no error  -->
    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
                  BuildUri="$(BuildUri)"
                  Id="$(StyleCopStep)"
                  Status="Succeeded"
                  Message="StyleCop Succeeded: $(AllPassed), Violations: $(Violations)"/>

    <!-- If an error has been raised we call this target 
         You might have thought you could so the same as the error line above and this followng
         OnError line by adding a condition as shown below. However this does not work
         as the OnError condition is not evaluated unless an error as previously occured-->
    <OnError ExecuteTargets="FailTheBuild" />
    <!--<OnError ExecuteTargets="FailTheBuild" Condition="'$(AllPassed)' == 'False'"  />-->

  </Target>

  <Target Name="FailTheBuild">
    <!-- We are failing the build due to stylecop issues -->
    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
            BuildUri="$(BuildUri)"
            Id="$(StyleCopStep)"
            Status="Failed"
            Message="StyleCop Failed: $(AllPassed), Violations: $(Violations) [See $(DropLocation)\$(BuildNumber)\StyleCopLog.txt]"/>

    <!-- List out the issues-->
    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
            BuildUri="$(BuildUri)"
            Message="%(Failures.Identity) - Failed on Line %(Failures.LineNumber). %(Failures.CheckId): %(Failures.Message)"/>

  </Target>


Finally to get the new in formation out of the build and into the build wallboard



public void UpdateStatus(IBuildDetail detail)
{
    // any other results fields updates
 
    // and now the custom nodes
    IBuildInformation info = detail.Information;
    foreach (IBuildInformationNode infoNode in info.Nodes)
    {
        if (infoNode.Type == "org.stylecop")
        {
            // we have the correct node
            this.SetStyleCopWarnings(infoNode.Fields["total-violations"]);
            break;
        }
    }
}


So not a perfect solution, but but does everything I need at present.