//-----------------------------------------------------------------------
// (c) http://TfsBuildExtensions.codeplex.com/. This source is subject to the Microsoft Permissive License. See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx. All other rights reserved.
//-----------------------------------------------------------------------
namespace TfsBuildExtensions.Activities.CodeQuality
{
using System;
using System.Activities;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Build.Workflow.Services;
using TfsBuildExtensions.Activities.CodeMetrics.Extended;
using TfsBuildExtensions.Activities.CodeQuality.Extended;
using System.Threading;
///
/// Activity for processing code metrics using the Visual Studio Code Metrics PowerTool 10.0
/// (http://www.microsoft.com/downloads/en/details.aspx?FamilyID=edd1dfb0-b9fe-4e90-b6a6-5ed6f6f6e615)
///
///
///
///
/// ]]>
///
///
[BuildActivity(HostEnvironmentOption.All)]
public class CodeMetrics : BaseCodeActivity
{
private const string MaintainabilityIndex = "MaintainabilityIndex";
private const string CyclomaticComplexity = "CyclomaticComplexity";
private InArgument searchGac = false;
private InArgument analyzeMetricsResult = true;
///
/// Path to where the binaries are placed
///
[Description("Path to where the binaries are placed")]
[RequiredArgument]
public InArgument BinariesDirectory { get; set; }
///
/// Optional: Which files that should be processed. Can be a list of files or file match patterns. Defaults to *.dll;*.exe
///
[Description("Which files that should be processed. Can be a list of files or file match patterns. Defaults to *.dll;*.exe")]
public InArgument> FilesToProcess { get; set; }
///
/// Optional: Which files that should be ignored. Can be a list of files or file match patterns.
///
[Description("Which files that should be ignored. Can be a list of files or file match patterns.")]
public InArgument> FilesToIgnore { get; set; }
///
/// Optional: Name of the generated metrics result file. Should end with .xml
///
[Description("Optional: Name of the generated metrics result file. Default Metrics.xml")]
public InArgument GeneratedFileName { get; set; }
///
/// Optional: Enables/Disables searchGac. Default false
///
[Description("Optional: Enables/Disables searchGac. Default false")]
public InArgument SearchGac
{
get { return this.searchGac; }
set { this.searchGac = value; }
}
///
/// Optional: Enables/Disables searchGac. Default false
///
[Description("Optional: Enables/Disables analysis of code metrics results. Default false")]
public InArgument AnalyzeMetricsResult
{
get { return this.analyzeMetricsResult; }
set { this.analyzeMetricsResult = value; }
}
///
/// Threshold value for what Maintainability Index should fail the build
///
[RequiredArgument]
public InArgument MaintainabilityIndexErrorThreshold { get; set; }
///
/// Threshold value for what Maintainability Index should partially fail the build
///
[RequiredArgument]
public InArgument MaintainabilityIndexWarningThreshold { get; set; }
///
/// Threshold value for what Cyclomatic Complexity should fail the build
///
[RequiredArgument]
public InArgument CyclomaticComplexityErrorThreshold { get; set; }
///
/// Threshold value for what Cyclomatic Complexity should partially fail the build
///
[RequiredArgument]
public InArgument CyclomaticComplexityWarningThreshold { get; set; }
///
/// Overrides the global thresholds for the Assembly Metric Level by specific one.
/// When a level is not overrides (value of 0), the global thresholds are used.
///
///
/// Raise this exception if the string contains a part that is not an integer.
///
[Description("Optional: Overrides the global thresholds for the Assembly Metric Level by specific one. The expected format is 9999;9999;9999;9999 where the values are metric's thresholds for the Maintainability Index Error, Maintainability Index Warning, Cyclo Complexity Error and Cyclo Complexity Warning.")]
public InArgument AssemblyThresholdsString { get; set; }
///
/// Overrides the global thresholds for the Namespace Metric Level by specific one.
/// When a level is not overrides (value of 0), the global thresholds are used.
///
///
/// Raise this exception if the string contains a part that is not an integer.
///
[Description("Optional: Overrides the global thresholds for the Namespace Metric Level by specific one. The expected format is 9999;9999;9999;9999 where the values are metric's thresholds for the Maintainability Index Error, Maintainability Index Warning, Cyclo Complexity Error and Cyclo Complexity Warning.")]
public InArgument NamespaceThresholdsString { get; set; }
///
/// Overrides the global thresholds for the Assembly Metric Level by specific one.
/// When a level is not overrides (value of 0), the global thresholds are used.
///
///
/// Raise this exception if the string contains a part that is not an integer.
///
[Description("Optional: Overrides the global thresholds for the Type Metric Level by specific one. The expected format is 9999;9999;9999;9999 where the values are metric's thresholds for the Maintainability Index Error, Maintainability Index Warning, Cyclo Complexity Error and Cyclo Complexity Warning.")]
public InArgument TypeThresholdsString { get; set; }
///
/// Overrides the global thresholds for the Assembly Metric Level by specific one.
/// When a level is not overrides (value of 0), the global thresholds are used.
///
///
/// Raise this exception if the string contains a part that is not an integer.
///
[Description("Optional: Overrides the global thresholds for the Member Metric Level by specific one. The expected format is 9999;9999;9999;9999 where the values are metric's thresholds for the Maintainability Index Error, Maintainability Index Warning, Cyclo Complexity Error and Cyclo Complexity Warning.")]
public InArgument MemberThresholdsString { get; set; }
///
/// Gets or sets ta value indicating if code metrics should be logged.
///
public InArgument LogCodeMetrics { get; set; }
private IBuildDetail BuildDetail { get; set; }
///
/// Path to Program Files environment directory.
///
/// Path to Program Files directory (C:\Program Files or C:\Program Files (x86)).
public static string ProgramFilesX86()
{
if (8 == IntPtr.Size || (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432"))))
{
return Environment.GetEnvironmentVariable("ProgramFiles(x86)");
}
return Environment.GetEnvironmentVariable("ProgramFiles");
}
///
/// Executes the logic for this workflow activity
///
protected override void InternalExecute()
{
this.BuildDetail = this.ActivityContext.GetExtension();
string generatedFile = "Metrics.xml";
if (this.GeneratedFileName != null && !string.IsNullOrEmpty(this.GeneratedFileName.Get(this.ActivityContext)))
{
generatedFile = Path.Combine(this.BuildDetail.DropLocation, this.GeneratedFileName.Get(this.ActivityContext));
}
if (!this.RunCodeMetrics(generatedFile))
{
return;
}
IActivityTracking currentTracking = this.ActivityContext.GetExtension().GetActivityTracking(this.ActivityContext);
if (!this.AnalyzeMetricsResult.Get(this.ActivityContext))
{
AddTextNode("Skipped code metrics analysis", currentTracking.Node);
return;
}
IBuildInformationNode rootNode = AddTextNode("Analyzing code metrics results", currentTracking.Node);
string fileName = Path.GetFileName(generatedFile);
string pathToFileInDropFolder = Path.Combine(this.BuildDetail.DropLocation, fileName);
AddLinkNode(fileName, new Uri(pathToFileInDropFolder), rootNode);
CodeMetricsReport result = CodeMetricsReport.LoadFromFile(generatedFile);
if (result == null)
{
LogBuildMessage("Could not load metric result file ");
return;
}
// Get thresholds for each level.
SpecificMetricThresholds assemblyMetricThresholds = CodeMetricsThresholds.GetForAssembly(this, this.ActivityContext);
SpecificMetricThresholds namespaceMetricThresholds = CodeMetricsThresholds.GetForNamespace(this, this.ActivityContext);
SpecificMetricThresholds typeMetricThresholds = CodeMetricsThresholds.GetForType(this, this.ActivityContext);
SpecificMetricThresholds memberMetricThresholds = CodeMetricsThresholds.GetForMember(this, this.ActivityContext);
// Check if metrics should be logged.
bool logCodeMetrics = this.ActivityContext.GetValue(this.LogCodeMetrics);
foreach (var target in result.Targets)
{
var targetNode = logCodeMetrics ? AddTextNode("Target: " + target.Name, rootNode) : null;
foreach (var module in target.Modules)
{
var moduleNode = logCodeMetrics ? AddTextNode("Module: " + module.Name, targetNode) : null;
this.ProcessMetrics(module.Name, module.Metrics, moduleNode, assemblyMetricThresholds);
foreach (var ns in module.Namespaces)
{
var namespaceNode = logCodeMetrics ? AddTextNode("Namespace: " + ns.Name, moduleNode) : null;
this.ProcessMetrics(ns.Name, ns.Metrics, namespaceNode, namespaceMetricThresholds);
foreach (var type in ns.Types)
{
var typeNode = logCodeMetrics ? AddTextNode("Type: " + type.Name, namespaceNode) : null;
this.ProcessMetrics(type.Name, type.Metrics, typeNode, typeMetricThresholds);
foreach (var member in type.Members)
{
var memberNode = logCodeMetrics ? AddTextNode("Member: " + member.Name, typeNode) : null;
this.ProcessMetrics(member.Name, member.Metrics, memberNode, memberMetricThresholds, type.Name);
}
}
}
}
}
}
///
/// Override for base.CacheMetadata
///
/// CodeActivityMetadata
protected override void CacheMetadata(CodeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
metadata.RequireExtension(typeof(IBuildDetail));
}
private static string GetMemberRootForOutput(string memberRootDesc)
{
if (!string.IsNullOrWhiteSpace(memberRootDesc))
{
return string.Format(" [Root:{0}]", memberRootDesc);
}
return string.Empty;
}
private bool RunCodeMetrics(string output)
{
string metricsExePath = Path.Combine(ProgramFilesX86(), @"Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\metrics.exe");
if (!File.Exists(metricsExePath))
{
LogBuildError("Could not locate " + metricsExePath + ". Please download Visual Studio Code Metrics PowerTool 10.0 at http://www.microsoft.com/downloads/en/details.aspx?FamilyID=edd1dfb0-b9fe-4e90-b6a6-5ed6f6f6e615");
return false;
}
string metricsExeArguments = this.GetFilesToProcess().Aggregate(string.Empty, (current, file) => current + string.Format(" /f:\"{0}\"", file));
metricsExeArguments += string.Format(" /out:\"{0}\"", output);
if (this.SearchGac.Get(this.ActivityContext))
{
metricsExeArguments += string.Format(" /searchgac");
}
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = metricsExePath;
psi.UseShellExecute = false;
psi.RedirectStandardInput = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.Arguments = metricsExeArguments;
psi.WorkingDirectory = this.BinariesDirectory.Get(this.ActivityContext);
this.LogBuildMessage("Running " + psi.FileName + " " + psi.Arguments);
using (Process process = Process.Start(psi))
{
using (ManualResetEvent mreOut = new ManualResetEvent(false), mreErr = new ManualResetEvent(false))
{
process.OutputDataReceived += (o, e) => { if (e.Data == null) mreOut.Set(); else LogBuildMessage(e.Data); };
process.BeginOutputReadLine();
process.ErrorDataReceived += (o, e) => { if (e.Data == null) mreErr.Set(); else LogBuildMessage(e.Data); };
process.BeginErrorReadLine();
//string line;
//while (input != null && null != (line = input.ReadLine()))
// process.StandardInput.WriteLine(line);
process.StandardInput.Close();
process.WaitForExit();
mreOut.WaitOne();
mreErr.WaitOne();
if (process.ExitCode != 0)
{
this.LogBuildError(process.ExitCode.ToString(CultureInfo.CurrentCulture));
return false;
}
if (!File.Exists(output))
{
LogBuildError("Could not locate file " + output);
return false;
}
}
}
return true;
}
private IEnumerable GetFilesToProcess()
{
var metricsFiles = new CodeMetricsFilesToProcess(this, this.ActivityContext);
return metricsFiles.Get();
}
///
/// Analyzes the resulting metrics file and compares the Maintainability Index and Cyclomatic Complexity against the threshold values
///
/// Name of the member (namespace, module, type...)
/// The metrics for this member
/// The parent node in the build log
/// The thresholds for this level, member
private void ProcessMetrics(string member, IEnumerable metrics, IBuildInformationNode parent, SpecificMetricThresholds thresholds)
{
this.ProcessMetrics(member, metrics, parent, thresholds, string.Empty);
}
///
/// Analyzes the resulting metrics file and compares the Maintainability Index and Cyclomatic Complexity against the threshold values
///
/// Name of the member (namespace, module, type...)
/// The metrics for this member
/// The parent node in the build log
/// The thresholds for this level, member
/// The memberRootDesc
private void ProcessMetrics(string member, IEnumerable metrics, IBuildInformationNode parent, SpecificMetricThresholds thresholds, string memberRootDesc)
{
foreach (var metric in metrics)
{
int metricValue;
if (metric != null && !string.IsNullOrEmpty(metric.Value) && int.TryParse(metric.Value, out metricValue))
{
if (metric.Name == MaintainabilityIndex && metricValue < thresholds.MaintainabilityIndexErrorThreshold)
{
LogBuildError(string.Format("{0} for {1} is {2} which is below threshold ({3}){4}", MaintainabilityIndex, member, metric.Value, thresholds.MaintainabilityIndexErrorThreshold, GetMemberRootForOutput(memberRootDesc)));
if (this.FailBuildOnError.Get(this.ActivityContext)) this.FailCurrentBuild();
}
if (metric.Name == MaintainabilityIndex && metricValue < thresholds.MaintainabilityIndexWarningThreshold)
{
//this.PartiallyFailCurrentBuild();
LogBuildWarning(string.Format("{0} for {1} is {2} which is below threshold ({3}){4}", MaintainabilityIndex, member, metric.Value, thresholds.MaintainabilityIndexWarningThreshold, GetMemberRootForOutput(memberRootDesc)));
}
if (metric.Name == CyclomaticComplexity && metricValue > thresholds.CyclomaticComplexityErrorThreshold)
{
this.LogBuildError(string.Format("{0} for {1} is {2} which is above threshold ({3}){4}", CyclomaticComplexity, member, metric.Value, thresholds.CyclomaticComplexityErrorThreshold, GetMemberRootForOutput(memberRootDesc)));
if (this.FailBuildOnError.Get(this.ActivityContext)) this.FailCurrentBuild();
}
if (metric.Name == CyclomaticComplexity && metricValue > thresholds.CyclomaticComplexityWarningThreshold)
{
//this.PartiallyFailCurrentBuild();
this.LogBuildWarning(string.Format("{0} for {1} is {2} which is above threshold ({3}){4}", CyclomaticComplexity, member, metric.Value, thresholds.CyclomaticComplexityWarningThreshold, GetMemberRootForOutput(memberRootDesc)));
}
if (this.LogCodeMetrics.Get(this.ActivityContext))
{
AddTextNode(metric.Name + ": " + metric.Value, parent);
}
}
}
}
private void PartiallyFailCurrentBuild()
{
// Don't replace a failed status.
if (this.BuildDetail.Status != BuildStatus.Failed)
{
this.BuildDetail.Status = BuildStatus.PartiallySucceeded;
this.BuildDetail.Save();
}
}
private void FailCurrentBuild()
{
this.BuildDetail.Status = BuildStatus.Failed;
this.BuildDetail.Save();
}
}
}