//----------------------------------------------------------------------- // (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(); } } }