using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.IO;
using System.Diagnostics;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.Build.Client;
using System.Text.RegularExpressions;
namespace TypemockBuildActivity
{ public enum ExternalTestRunnerReturnCode { Unknown =0 , NotRun, Passed, Failed };
[BuildExtension(HostEnvironmentOption.Agent)]
[BuildActivity(HostEnvironmentOption.All)]
public sealed class ExternalTestRunner : CodeActivity<ExternalTestRunnerReturnCode>
{ // Define an activity input argument of type string
/// <summary>
/// The name of the wrapper application, usually tmockrunner.exe
/// </summary>
public InArgument<string> TestRunnerExecutable { get; set; }
/// <summary>
/// The name of the application that actually runs the test, defaults to MSTest.exe if not set
/// </summary>
public InArgument<string> MsTestExecutable { get; set; }
/// <summary>
/// The project collection to publish to e.g. http://tfs2010:8080/tfs/DefaultCollection
/// </summary>
public InArgument<string> ProjectCollection { get; set; }
/// <summary>
/// The build ID to to publish to e.g. vstfs:///Build/Build/91
/// </summary>
public InArgument<string> BuildNumber { get; set; }
/// <summary>
/// The project name to publish to e.g: "Typemock Test"
/// </summary>
public InArgument<string> TeamProjectName { get; set; }
/// <summary>
/// The platform name to publish to e.g. Any CPU
/// </summary>
public InArgument<string> Platform { get; set; }
/// <summary>
/// The flavour (configuration) to publish to e.g. "Debug"
/// </summary>
public InArgument<string> Flavor { get; set; }
/// <summary>
/// Array of assembly names to test
/// </summary>
public InArgument<string[]> TestAssemblyNames { get; set; }
/// <summary>
/// Where to search for assemblies under test
/// </summary>
public InArgument<string> SearchPathRoot { get; set; }
/// <summary>
/// A single name result file
/// </summary>
public InArgument<string> ResultsFile { get; set; }
/// <summary>
/// A directory to store results in (tends not be used if the ResultFile is set)
/// </summary>
public InArgument<string> ResultsFileRoot { get; set; }
/// <summary>
/// The file that list as to how test should be run
/// </summary>
public InArgument<string> TestSettings { get; set; }
// If your activity returns a value, derive from CodeActivity<TResult>
// and return the value from the Execute method.
protected override ExternalTestRunnerReturnCode Execute(CodeActivityContext context)
{ String msTestOutput = string.Empty;
ExternalTestRunnerReturnCode exitMessage = ExternalTestRunnerReturnCode.NotRun;
if (CheckFileExists(TestRunnerExecutable.Get(context)) == false)
{ LogError(context, string.Format("TestRunner not found {0}", TestRunnerExecutable.Get(context))); }
else
{ String mstest = MsTestExecutable.Get(context);
if (CheckFileExists(mstest) == false)
{ mstest = GetDefaultMsTestPath();
}
String testrunner = TestRunnerExecutable.Get(context);
var arguments = new StringBuilder();
arguments.Append(string.Format("\"{0}\"", mstest)); arguments.Append(" /nologo ");
// the files to test
foreach (string name in TestAssemblyNames.Get(context))
{ arguments.Append(AddParameterIfNotNull("testcontainer", name)); }
// settings about what to test
arguments.Append(AddParameterIfNotNull("searchpathroot", SearchPathRoot.Get(context))); arguments.Append(AddParameterIfNotNull("testSettings", TestSettings.Get(context)));
// now the publish bits
if (string.IsNullOrEmpty(ProjectCollection.Get(context)) == false)
{ arguments.Append(AddParameterIfNotNull("publish", ProjectCollection.Get(context))); arguments.Append(AddParameterIfNotNull("publishbuild", BuildNumber.Get(context))); arguments.Append(AddParameterIfNotNull("teamproject", TeamProjectName.Get(context))); arguments.Append(AddParameterIfNotNull("platform", Platform.Get(context))); arguments.Append(AddParameterIfNotNull("flavor", Flavor.Get(context))); }
// where do the results go, tend to use one of these not both
arguments.Append(AddParameterIfNotNull("resultsfile", ResultsFile.Get(context))); arguments.Append(AddParameterIfNotNull("resultsfileroot", ResultsFileRoot.Get(context)));
LogMessage(context, string.Format("Call Mstest With Wrapper [{0}] and arguments [{1}]", testrunner, arguments.ToString()), BuildMessageImportance.Normal);
using (System.Diagnostics.Process process = new System.Diagnostics.Process())
{ process.StartInfo.FileName = testrunner;
process.StartInfo.WorkingDirectory = SearchPathRoot.Get(context);
process.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
process.StartInfo.UseShellExecute = false;
process.StartInfo.ErrorDialog = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.Arguments = arguments.ToString();
try
{ process.Start();
msTestOutput = process.StandardOutput.ReadToEnd();
process.WaitForExit();
// for TypemockRunner and MSTest this is alway seems to be 1 so does not help tell if test passed or not
// In general you can detect test failures by simply checking whether mstest.exe returned 0 or not.
// I say in general because there is a known bug where on certain OSes mstest.exe sometimes returns 128 whether
// successful or not, so mstest.exe 10.0 added a new command-line option /usestderr which causes it to write
// something to standard error on failure.
// If (error data received)
// FAIL
// Else If (exit code != 0 AND exit code != 128)
// FAIL
// Else If (exit code == 128)
// Write Warning about weird error code, but SUCCEED
// Else
// SUCCEED
///int exitCode = process.ExitCode;
LogMessage(context, string.Format("Output of ExternalTestRunner: {0}", msTestOutput), BuildMessageImportance.High); }
catch (InvalidOperationException ex)
{ LogError(context, "ExternalTestRunner InvalidOperationException :" + ex.Message);
}
exitMessage = ParseResultsForSummary(msTestOutput);
}
}
LogMessage(context, string.Format("ExternaTestRunner exiting with message [{0}]", exitMessage), BuildMessageImportance.High); return exitMessage;
}
/// <summary>
/// Adds a parameter to the MSTest line, it has been extracted to allow us to do a isEmpty chekc in one place
/// </summary>
/// <param name="parameterName">The name of the parameter</param>
/// <param name="value">The string value</param>
/// <returns>If the value is present a formated block is return</returns>
private static string AddParameterIfNotNull(string parameterName, string value)
{ var returnValue = string.Empty;
if (string.IsNullOrEmpty(value) == false)
{ returnValue = string.Format(" /{0}:\"{1}\"", parameterName, value); }
return returnValue;
}
/// <summary>
/// A handler to check the results for the success or failure message
/// This is a rough way to do it, but is more reliable than the MSTest exit codes
/// It returns a string as opposed to an exit code so that it
/// Note this will not work of the /usestderr flag is used
/// </summary>
/// <param name="output">The output from the test run</param>
/// <returns>A single line summary</returns>
private static ExternalTestRunnerReturnCode ParseResultsForSummary(String output)
{ ExternalTestRunnerReturnCode exitMessage = ExternalTestRunnerReturnCode.NotRun;
if (Regex.IsMatch(output, "Test Run Failed"))
{ exitMessage = ExternalTestRunnerReturnCode.Failed;
}
else if (Regex.IsMatch(output, "Test Run Completed"))
{ exitMessage = ExternalTestRunnerReturnCode.Passed;
}
else
{ exitMessage = ExternalTestRunnerReturnCode.Unknown;
}
return exitMessage;
}
/// <summary>
/// Handles finding MSTest, checking both the 32 and 64 bit paths
/// </summary>
/// <returns></returns>
private static string GetDefaultMsTestPath()
{ String mstest = @"C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\mstest.exe";
if (CheckFileExists(mstest) == false)
{ mstest = @"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\MSTest.exe";
if (CheckFileExists(mstest) == false)
{ throw new System.IO.FileNotFoundException("MsTest file cannot be found"); }
}
return mstest;
}
/// <summary>
/// Helper method so we log in both the VS Build and Debugger modes
/// </summary>
/// <param name="context">The workflow context</param>
/// <param name="message">Our message</param>
/// <param name="logLevel">Team build importance level</param>
private static void LogMessage(CodeActivityContext context, string message, BuildMessageImportance logLevel)
{ TrackingExtensions.TrackBuildMessage(context, message, logLevel);
Debug.WriteLine(message);
}
/// <summary>
/// Helper method so we log in both the VS Build and Debugger modes
/// </summary>
/// <param name="context">The workflow context</param>
/// <param name="message">Our message</param>
private static void LogError(CodeActivityContext context, string message)
{ TrackingExtensions.TrackBuildError(context, message);
Debug.WriteLine(message);
}
/// <summary>
/// Helper to check a file name to make sure it not null and that the file it name is present
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
private static bool CheckFileExists(string fileName)
{ return !((string.IsNullOrEmpty(fileName) == true) || (File.Exists(fileName) == false));
}
}
}