Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Jogge/xUnit-TeamCity #20

Merged
merged 10 commits into from
Mar 31, 2017
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# xUnit TeamCity plugin
A xUnit runner for TeamCity. Supports tests written with xUnit 1.9.2, 2.0.0 and 2.1.0, also supports wildcard include and exclude patterns.
A xUnit runner for TeamCity. Supports tests written with xUnit 1.9.2, 2.0.0, 2.1.0 and 2.2.0, also supports wildcard include and exclude patterns.

# Download
Get the latest release here: https://github.com/carlpett/xUnit-TeamCity/releases/latest.
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
package se.capeit.dev.xunittestrunner;

import com.intellij.openapi.util.SystemInfo;
import jetbrains.buildServer.agent.AgentRunningBuild;
import jetbrains.buildServer.agent.BuildFinishedStatus;
import jetbrains.buildServer.agent.BuildProgressLogger;
import jetbrains.buildServer.agent.BuildRunnerContext;
import jetbrains.buildServer.messages.DefaultMessagesInfo;
import jetbrains.buildServer.util.AntPatternFileFinder;
import jetbrains.buildServer.util.StringUtil;
import jetbrains.buildServer.util.CollectionsUtil;
import jetbrains.buildServer.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import com.intellij.openapi.util.SystemInfo;

import java.io.File;
import java.io.InputStream;
import java.util.Map;
import java.util.List;
import java.util.Scanner;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class XUnitBuildProcess extends FutureBasedBuildProcess {
private final AgentRunningBuild buildingAgent;
private final BuildRunnerContext context;
private Process testRunnerProcess;
private HashMap<Process, String> processes = new HashMap<>();

public XUnitBuildProcess(@NotNull final BuildRunnerContext context) {
super(context);
Expand All @@ -29,24 +28,32 @@ public XUnitBuildProcess(@NotNull final BuildRunnerContext context) {
this.buildingAgent = context.getBuild();
}

private String getParameter(@NotNull final String parameterName)
{
private String getParameter(@NotNull final String parameterName) {
final String value = context.getRunnerParameters().get(parameterName);
if (value == null || value.trim().length() == 0) return "";
return value.trim();
}

private List<String> getAssemblies(final String rawAssemblyParameter) {
String withSlashesFixed = rawAssemblyParameter.replace('\\','/');
String withSlashesFixed = rawAssemblyParameter.replace('\\', '/');
List<String> assemblies = StringUtil.split(withSlashesFixed, true, ',', ';', '\n', '\r');
return assemblies;
}

protected void cancelBuild() {
if (testRunnerProcess == null)
return;
for (Map.Entry<Process, String> p : processes.entrySet()) {
p.getKey().destroy();
}
processes.clear();
}

testRunnerProcess.destroy();
private int tryParseInt(String stringValue, int defaultValue) {
try {
return Integer.parseInt(stringValue);
}
catch (NumberFormatException e) {
return defaultValue;
}
}

public BuildFinishedStatus call() throws Exception {
Expand All @@ -57,41 +64,57 @@ public BuildFinishedStatus call() throws Exception {
String platform = getParameter(StringConstants.ParameterName_Platform);
logger.message("Runner parameters { Version = " + version + ", runtime = " + runtime + ", platform = " + platform + "}");

int numberOfParallelProcesses = tryParseInt(getParameter(StringConstants.ParameterName_NumberOfParallelProcesses), 1);
logger.message("Number of parallel processes is set to: " + numberOfParallelProcesses);

File agentToolsDirectory = buildingAgent.getAgentConfiguration().getAgentToolsDirectory();
String runnerPath = new File(agentToolsDirectory, "xunit-runner\\bin\\" + version + "\\" + runner.getRunnerPath(runtime, platform)).getPath();
logger.message("Starting test runner at " + runnerPath);

List<String> assemblies = getAssemblies(getParameter(StringConstants.ParameterName_IncludedAssemblies));
String commandLineArguments = getParameter(StringConstants.ParameterName_CommandLineArguments);
List<String> excludedAssemblies = getAssemblies(getParameter(StringConstants.ParameterName_ExcludedAssemblies));
excludedAssemblies.add("**/obj/**"); // We always exclude **/obj/**

BuildFinishedStatus status = BuildFinishedStatus.FINISHED_SUCCESS;

// Find the files, and run them through the test runner
AntPatternFileFinder finder = new AntPatternFileFinder(
CollectionsUtil.toStringArray(assemblies),
CollectionsUtil.toStringArray(excludedAssemblies),
SystemInfo.isFileSystemCaseSensitive);
CollectionsUtil.toStringArray(assemblies),
CollectionsUtil.toStringArray(excludedAssemblies),
SystemInfo.isFileSystemCaseSensitive);
File[] assemblyFiles = finder.findFiles(context.getWorkingDirectory());
if(assemblyFiles.length == 0) {
if (assemblyFiles.length == 0) {
logger.warning("No assemblies were matched - no tests will be run!");
}
for(File assembly : assemblyFiles) {

for (File assembly : assemblyFiles) {
String activityBlockName = "Testing " + assembly.getName();
logger.activityStarted(activityBlockName, assembly.getAbsolutePath(), DefaultMessagesInfo.BLOCK_TYPE_MODULE);

String filePath = assembly.getAbsolutePath();
String commandLineFlags = getCommandLineFlags(version);
logger.message("Commandline: " + runnerPath + " " + filePath + " " + commandLineFlags);
ProcessBuilder processBuilder = new ProcessBuilder(runnerPath, filePath, commandLineFlags);
logger.message("Commandline: " + runnerPath + " " + filePath + " " + commandLineFlags + " " + commandLineArguments);

List<String> commandLine = new ArrayList<>();
commandLine.add(runnerPath);
commandLine.add(filePath);
commandLine.add(commandLineFlags);

Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(commandLineArguments);
while (m.find())
commandLine.add(m.group(1).replace("\"", ""));

ProcessBuilder processBuilder = new ProcessBuilder(commandLine);

// Copy environment variables
Map<String, String> env = processBuilder.environment();
for(Map.Entry<String, String> kvp : context.getBuildParameters().getEnvironmentVariables().entrySet()) {
for (Map.Entry<String, String> kvp : context.getBuildParameters().getEnvironmentVariables().entrySet()) {
env.put(kvp.getKey(), kvp.getValue());
}

testRunnerProcess = processBuilder.start();
Process testRunnerProcess = processBuilder.start();
processes.put(testRunnerProcess, activityBlockName);

redirectStreamToLogger(testRunnerProcess.getInputStream(), new RedirectionTarget() {
public void redirect(String s) {
Expand All @@ -103,28 +126,56 @@ public void redirect(String s) {
logger.warning(s);
}
});

int exitCode = testRunnerProcess.waitFor();
if(exitCode != 0) {
logger.warning("Test runner exited with non-zero status!");
status = BuildFinishedStatus.FINISHED_FAILED;

while (true) {
int liveProcessCount = 0;
for (Map.Entry<Process, String> p : processes.entrySet()) {
if (isRunning(p.getKey())) {
++liveProcessCount;
}
}
if (liveProcessCount < numberOfParallelProcesses) break;
Thread.sleep(100);
}

logger.activityFinished(activityBlockName, DefaultMessagesInfo.BLOCK_TYPE_MODULE);
}

for (Map.Entry<Process, String> p : processes.entrySet()) {
int exitCode = p.getKey().waitFor();
if (version.charAt(0) == '2' && version.charAt(2) == '2') {
// From 2.2 the exit code actually indicates if there was an error with the command line / runtime. https://github.com/xunit/xunit/issues/659
if(exitCode > 1) {
logger.warning("Test runner exited with runtime error! Returned status code was " + exitCode);
status = BuildFinishedStatus.FINISHED_FAILED;
}
}
// Checking the exit code on versions below 2.2 is actually useless, as they break the TeamCity function to ignore failed tests.
// The exit code on older versions of xunit always indicates the number of failed tests.

logger.activityFinished(p.getValue(), DefaultMessagesInfo.BLOCK_TYPE_MODULE);
}
return status;
}
catch(Exception e) {
} catch (Exception e) {
logger.message("Failed to run tests");
logger.exception(e);
return BuildFinishedStatus.FINISHED_FAILED;
} finally {
processes.clear();
}
}

boolean isRunning(Process process) {
try {
process.exitValue();
return false;
} catch (Exception e) {
return true;
}
}

private interface RedirectionTarget {
void redirect(String s);
}

private void redirectStreamToLogger(final InputStream s, final RedirectionTarget target) {
new Thread(new Runnable() {
public void run() {
Expand All @@ -141,7 +192,7 @@ private String getCommandLineFlags(String version) {
// This is quite crude at the moment, but does the job.
// TODO: Migrate this into RunnerVersion or similar
char majorVersion = version.charAt(0);
if(majorVersion == '1')
if (majorVersion == '1')
return "/teamcity";
return "-teamcity";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ public String getRunnerPath(String runtime, String platform) {
sb.append(".exe");
return sb.toString();
}
});

AvailableRunners.put("2.2.0", new RunnerVersion("2.2.0",
new String[]{Runtime.dotNET45},
new String[]{Platforms.x86, Platforms.MSIL}) {
@Override
public String getRunnerPath(String runtime, String platform) {
StringBuilder sb = new StringBuilder();
sb.append("xunit.console");
if (platform.equals(Platforms.x86))
sb.append(".x86");
sb.append(".exe");
return sb.toString();
}
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ public final class StringConstants {
public static final String RunTypeName = "xUnitRunner";
public static final String ToolName = "xunit-runner"; // Should mirror the xunit-runner artifactId

public static final String ParameterName_XUnitVersion = "xUnitVersion";
public static final String ParameterName_XUnitVersion = "xUnitVersion";
public static final String ParameterName_CommandLineArguments = "commandLineArguments";
public static final String ParameterName_NumberOfParallelProcesses = "numberOfParallelProcesses";
public static final String ParameterName_IncludedAssemblies = "includedAssemblies";
public static final String ParameterName_ExcludedAssemblies = "excludedAssemblies";
public static final String ParameterName_Platform = "runnerPlatform";
Expand All @@ -14,6 +16,12 @@ public final class StringConstants {
public String getParameterName_XUnitVersion() {
return ParameterName_XUnitVersion;
}
public String getParameterName_CommandLineArguments() {
return ParameterName_CommandLineArguments;
}
public String getParameterName_NumberOfParallelProcesses() {
return ParameterName_NumberOfParallelProcesses;
}
public String getParameterName_IncludedAssemblies() {
return ParameterName_IncludedAssemblies;
}
Expand Down
Loading