Skip to content

Commit 6f1fb84

Browse files
authored
Merge pull request #20 from carlpett/merge-fork-jogge
Merge Jogge/xUnit-TeamCity
2 parents 6915177 + 495b0b1 commit 6f1fb84

File tree

12 files changed

+746
-37
lines changed

12 files changed

+746
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# xUnit TeamCity plugin
2-
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.
2+
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.
33

44
# Download
55
Get the latest release here: https://github.com/carlpett/xUnit-TeamCity/releases/latest.

xunit-agent/src/main/java/se/capeit/dev/xunittestrunner/XUnitBuildProcess.java

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
package se.capeit.dev.xunittestrunner;
22

3+
import com.intellij.openapi.util.SystemInfo;
34
import jetbrains.buildServer.agent.AgentRunningBuild;
45
import jetbrains.buildServer.agent.BuildFinishedStatus;
5-
import jetbrains.buildServer.agent.BuildProgressLogger;
66
import jetbrains.buildServer.agent.BuildRunnerContext;
77
import jetbrains.buildServer.messages.DefaultMessagesInfo;
88
import jetbrains.buildServer.util.AntPatternFileFinder;
9-
import jetbrains.buildServer.util.StringUtil;
109
import jetbrains.buildServer.util.CollectionsUtil;
10+
import jetbrains.buildServer.util.StringUtil;
1111
import org.jetbrains.annotations.NotNull;
12-
import com.intellij.openapi.util.SystemInfo;
1312

1413
import java.io.File;
1514
import java.io.InputStream;
16-
import java.util.Map;
17-
import java.util.List;
18-
import java.util.Scanner;
15+
import java.util.*;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
1918

2019
class XUnitBuildProcess extends FutureBasedBuildProcess {
2120
private final AgentRunningBuild buildingAgent;
2221
private final BuildRunnerContext context;
23-
private Process testRunnerProcess;
22+
private HashMap<Process, String> processes = new HashMap<>();
2423

2524
public XUnitBuildProcess(@NotNull final BuildRunnerContext context) {
2625
super(context);
@@ -29,24 +28,32 @@ public XUnitBuildProcess(@NotNull final BuildRunnerContext context) {
2928
this.buildingAgent = context.getBuild();
3029
}
3130

32-
private String getParameter(@NotNull final String parameterName)
33-
{
31+
private String getParameter(@NotNull final String parameterName) {
3432
final String value = context.getRunnerParameters().get(parameterName);
3533
if (value == null || value.trim().length() == 0) return "";
3634
return value.trim();
3735
}
3836

3937
private List<String> getAssemblies(final String rawAssemblyParameter) {
40-
String withSlashesFixed = rawAssemblyParameter.replace('\\','/');
38+
String withSlashesFixed = rawAssemblyParameter.replace('\\', '/');
4139
List<String> assemblies = StringUtil.split(withSlashesFixed, true, ',', ';', '\n', '\r');
4240
return assemblies;
4341
}
4442

4543
protected void cancelBuild() {
46-
if (testRunnerProcess == null)
47-
return;
44+
for (Map.Entry<Process, String> p : processes.entrySet()) {
45+
p.getKey().destroy();
46+
}
47+
processes.clear();
48+
}
4849

49-
testRunnerProcess.destroy();
50+
private int tryParseInt(String stringValue, int defaultValue) {
51+
try {
52+
return Integer.parseInt(stringValue);
53+
}
54+
catch (NumberFormatException e) {
55+
return defaultValue;
56+
}
5057
}
5158

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

67+
int numberOfParallelProcesses = tryParseInt(getParameter(StringConstants.ParameterName_NumberOfParallelProcesses), 1);
68+
logger.message("Number of parallel processes is set to: " + numberOfParallelProcesses);
69+
6070
File agentToolsDirectory = buildingAgent.getAgentConfiguration().getAgentToolsDirectory();
6171
String runnerPath = new File(agentToolsDirectory, "xunit-runner\\bin\\" + version + "\\" + runner.getRunnerPath(runtime, platform)).getPath();
6272
logger.message("Starting test runner at " + runnerPath);
6373

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

6879
BuildFinishedStatus status = BuildFinishedStatus.FINISHED_SUCCESS;
6980

7081
// Find the files, and run them through the test runner
7182
AntPatternFileFinder finder = new AntPatternFileFinder(
72-
CollectionsUtil.toStringArray(assemblies),
73-
CollectionsUtil.toStringArray(excludedAssemblies),
74-
SystemInfo.isFileSystemCaseSensitive);
83+
CollectionsUtil.toStringArray(assemblies),
84+
CollectionsUtil.toStringArray(excludedAssemblies),
85+
SystemInfo.isFileSystemCaseSensitive);
7586
File[] assemblyFiles = finder.findFiles(context.getWorkingDirectory());
76-
if(assemblyFiles.length == 0) {
87+
if (assemblyFiles.length == 0) {
7788
logger.warning("No assemblies were matched - no tests will be run!");
7889
}
79-
for(File assembly : assemblyFiles) {
90+
91+
for (File assembly : assemblyFiles) {
8092
String activityBlockName = "Testing " + assembly.getName();
8193
logger.activityStarted(activityBlockName, assembly.getAbsolutePath(), DefaultMessagesInfo.BLOCK_TYPE_MODULE);
8294

8395
String filePath = assembly.getAbsolutePath();
8496
String commandLineFlags = getCommandLineFlags(version);
85-
logger.message("Commandline: " + runnerPath + " " + filePath + " " + commandLineFlags);
86-
ProcessBuilder processBuilder = new ProcessBuilder(runnerPath, filePath, commandLineFlags);
97+
logger.message("Commandline: " + runnerPath + " " + filePath + " " + commandLineFlags + " " + commandLineArguments);
98+
99+
List<String> commandLine = new ArrayList<>();
100+
commandLine.add(runnerPath);
101+
commandLine.add(filePath);
102+
commandLine.add(commandLineFlags);
103+
104+
Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(commandLineArguments);
105+
while (m.find())
106+
commandLine.add(m.group(1).replace("\"", ""));
107+
108+
ProcessBuilder processBuilder = new ProcessBuilder(commandLine);
87109

88110
// Copy environment variables
89111
Map<String, String> env = processBuilder.environment();
90-
for(Map.Entry<String, String> kvp : context.getBuildParameters().getEnvironmentVariables().entrySet()) {
112+
for (Map.Entry<String, String> kvp : context.getBuildParameters().getEnvironmentVariables().entrySet()) {
91113
env.put(kvp.getKey(), kvp.getValue());
92114
}
93115

94-
testRunnerProcess = processBuilder.start();
116+
Process testRunnerProcess = processBuilder.start();
117+
processes.put(testRunnerProcess, activityBlockName);
95118

96119
redirectStreamToLogger(testRunnerProcess.getInputStream(), new RedirectionTarget() {
97120
public void redirect(String s) {
@@ -103,28 +126,56 @@ public void redirect(String s) {
103126
logger.warning(s);
104127
}
105128
});
106-
107-
int exitCode = testRunnerProcess.waitFor();
108-
if(exitCode != 0) {
109-
logger.warning("Test runner exited with non-zero status!");
110-
status = BuildFinishedStatus.FINISHED_FAILED;
129+
130+
while (true) {
131+
int liveProcessCount = 0;
132+
for (Map.Entry<Process, String> p : processes.entrySet()) {
133+
if (isRunning(p.getKey())) {
134+
++liveProcessCount;
135+
}
136+
}
137+
if (liveProcessCount < numberOfParallelProcesses) break;
138+
Thread.sleep(100);
111139
}
112-
113-
logger.activityFinished(activityBlockName, DefaultMessagesInfo.BLOCK_TYPE_MODULE);
114140
}
115141

142+
for (Map.Entry<Process, String> p : processes.entrySet()) {
143+
int exitCode = p.getKey().waitFor();
144+
if (version.charAt(0) == '2' && version.charAt(2) == '2') {
145+
// 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
146+
if(exitCode > 1) {
147+
logger.warning("Test runner exited with runtime error! Returned status code was " + exitCode);
148+
status = BuildFinishedStatus.FINISHED_FAILED;
149+
}
150+
}
151+
// Checking the exit code on versions below 2.2 is actually useless, as they break the TeamCity function to ignore failed tests.
152+
// The exit code on older versions of xunit always indicates the number of failed tests.
153+
154+
logger.activityFinished(p.getValue(), DefaultMessagesInfo.BLOCK_TYPE_MODULE);
155+
}
116156
return status;
117-
}
118-
catch(Exception e) {
157+
} catch (Exception e) {
119158
logger.message("Failed to run tests");
120159
logger.exception(e);
121160
return BuildFinishedStatus.FINISHED_FAILED;
161+
} finally {
162+
processes.clear();
163+
}
164+
}
165+
166+
boolean isRunning(Process process) {
167+
try {
168+
process.exitValue();
169+
return false;
170+
} catch (Exception e) {
171+
return true;
122172
}
123173
}
124174

125175
private interface RedirectionTarget {
126176
void redirect(String s);
127177
}
178+
128179
private void redirectStreamToLogger(final InputStream s, final RedirectionTarget target) {
129180
new Thread(new Runnable() {
130181
public void run() {
@@ -141,7 +192,7 @@ private String getCommandLineFlags(String version) {
141192
// This is quite crude at the moment, but does the job.
142193
// TODO: Migrate this into RunnerVersion or similar
143194
char majorVersion = version.charAt(0);
144-
if(majorVersion == '1')
195+
if (majorVersion == '1')
145196
return "/teamcity";
146197
return "-teamcity";
147198
}

xunit-common/src/main/java/se/capeit/dev/xunittestrunner/Runners.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ public String getRunnerPath(String runtime, String platform) {
7171
sb.append(".exe");
7272
return sb.toString();
7373
}
74+
});
75+
76+
AvailableRunners.put("2.2.0", new RunnerVersion("2.2.0",
77+
new String[]{Runtime.dotNET45},
78+
new String[]{Platforms.x86, Platforms.MSIL}) {
79+
@Override
80+
public String getRunnerPath(String runtime, String platform) {
81+
StringBuilder sb = new StringBuilder();
82+
sb.append("xunit.console");
83+
if (platform.equals(Platforms.x86))
84+
sb.append(".x86");
85+
sb.append(".exe");
86+
return sb.toString();
87+
}
7488
});
7589
}
7690
}

xunit-common/src/main/java/se/capeit/dev/xunittestrunner/StringConstants.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ public final class StringConstants {
44
public static final String RunTypeName = "xUnitRunner";
55
public static final String ToolName = "xunit-runner"; // Should mirror the xunit-runner artifactId
66

7-
public static final String ParameterName_XUnitVersion = "xUnitVersion";
7+
public static final String ParameterName_XUnitVersion = "xUnitVersion";
8+
public static final String ParameterName_CommandLineArguments = "commandLineArguments";
9+
public static final String ParameterName_NumberOfParallelProcesses = "numberOfParallelProcesses";
810
public static final String ParameterName_IncludedAssemblies = "includedAssemblies";
911
public static final String ParameterName_ExcludedAssemblies = "excludedAssemblies";
1012
public static final String ParameterName_Platform = "runnerPlatform";
@@ -14,6 +16,12 @@ public final class StringConstants {
1416
public String getParameterName_XUnitVersion() {
1517
return ParameterName_XUnitVersion;
1618
}
19+
public String getParameterName_CommandLineArguments() {
20+
return ParameterName_CommandLineArguments;
21+
}
22+
public String getParameterName_NumberOfParallelProcesses() {
23+
return ParameterName_NumberOfParallelProcesses;
24+
}
1725
public String getParameterName_IncludedAssemblies() {
1826
return ParameterName_IncludedAssemblies;
1927
}

0 commit comments

Comments
 (0)