1
1
package se .capeit .dev .xunittestrunner ;
2
2
3
+ import com .intellij .openapi .util .SystemInfo ;
3
4
import jetbrains .buildServer .agent .AgentRunningBuild ;
4
5
import jetbrains .buildServer .agent .BuildFinishedStatus ;
5
- import jetbrains .buildServer .agent .BuildProgressLogger ;
6
6
import jetbrains .buildServer .agent .BuildRunnerContext ;
7
7
import jetbrains .buildServer .messages .DefaultMessagesInfo ;
8
8
import jetbrains .buildServer .util .AntPatternFileFinder ;
9
- import jetbrains .buildServer .util .StringUtil ;
10
9
import jetbrains .buildServer .util .CollectionsUtil ;
10
+ import jetbrains .buildServer .util .StringUtil ;
11
11
import org .jetbrains .annotations .NotNull ;
12
- import com .intellij .openapi .util .SystemInfo ;
13
12
14
13
import java .io .File ;
15
14
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 ;
19
18
20
19
class XUnitBuildProcess extends FutureBasedBuildProcess {
21
20
private final AgentRunningBuild buildingAgent ;
22
21
private final BuildRunnerContext context ;
23
- private Process testRunnerProcess ;
22
+ private HashMap < Process , String > processes = new HashMap <>() ;
24
23
25
24
public XUnitBuildProcess (@ NotNull final BuildRunnerContext context ) {
26
25
super (context );
@@ -29,24 +28,32 @@ public XUnitBuildProcess(@NotNull final BuildRunnerContext context) {
29
28
this .buildingAgent = context .getBuild ();
30
29
}
31
30
32
- private String getParameter (@ NotNull final String parameterName )
33
- {
31
+ private String getParameter (@ NotNull final String parameterName ) {
34
32
final String value = context .getRunnerParameters ().get (parameterName );
35
33
if (value == null || value .trim ().length () == 0 ) return "" ;
36
34
return value .trim ();
37
35
}
38
36
39
37
private List <String > getAssemblies (final String rawAssemblyParameter ) {
40
- String withSlashesFixed = rawAssemblyParameter .replace ('\\' ,'/' );
38
+ String withSlashesFixed = rawAssemblyParameter .replace ('\\' , '/' );
41
39
List <String > assemblies = StringUtil .split (withSlashesFixed , true , ',' , ';' , '\n' , '\r' );
42
40
return assemblies ;
43
41
}
44
42
45
43
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
+ }
48
49
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
+ }
50
57
}
51
58
52
59
public BuildFinishedStatus call () throws Exception {
@@ -57,41 +64,57 @@ public BuildFinishedStatus call() throws Exception {
57
64
String platform = getParameter (StringConstants .ParameterName_Platform );
58
65
logger .message ("Runner parameters { Version = " + version + ", runtime = " + runtime + ", platform = " + platform + "}" );
59
66
67
+ int numberOfParallelProcesses = tryParseInt (getParameter (StringConstants .ParameterName_NumberOfParallelProcesses ), 1 );
68
+ logger .message ("Number of parallel processes is set to: " + numberOfParallelProcesses );
69
+
60
70
File agentToolsDirectory = buildingAgent .getAgentConfiguration ().getAgentToolsDirectory ();
61
71
String runnerPath = new File (agentToolsDirectory , "xunit-runner\\ bin\\ " + version + "\\ " + runner .getRunnerPath (runtime , platform )).getPath ();
62
72
logger .message ("Starting test runner at " + runnerPath );
63
73
64
74
List <String > assemblies = getAssemblies (getParameter (StringConstants .ParameterName_IncludedAssemblies ));
75
+ String commandLineArguments = getParameter (StringConstants .ParameterName_CommandLineArguments );
65
76
List <String > excludedAssemblies = getAssemblies (getParameter (StringConstants .ParameterName_ExcludedAssemblies ));
66
77
excludedAssemblies .add ("**/obj/**" ); // We always exclude **/obj/**
67
78
68
79
BuildFinishedStatus status = BuildFinishedStatus .FINISHED_SUCCESS ;
69
80
70
81
// Find the files, and run them through the test runner
71
82
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 );
75
86
File [] assemblyFiles = finder .findFiles (context .getWorkingDirectory ());
76
- if (assemblyFiles .length == 0 ) {
87
+ if (assemblyFiles .length == 0 ) {
77
88
logger .warning ("No assemblies were matched - no tests will be run!" );
78
89
}
79
- for (File assembly : assemblyFiles ) {
90
+
91
+ for (File assembly : assemblyFiles ) {
80
92
String activityBlockName = "Testing " + assembly .getName ();
81
93
logger .activityStarted (activityBlockName , assembly .getAbsolutePath (), DefaultMessagesInfo .BLOCK_TYPE_MODULE );
82
94
83
95
String filePath = assembly .getAbsolutePath ();
84
96
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 );
87
109
88
110
// Copy environment variables
89
111
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 ()) {
91
113
env .put (kvp .getKey (), kvp .getValue ());
92
114
}
93
115
94
- testRunnerProcess = processBuilder .start ();
116
+ Process testRunnerProcess = processBuilder .start ();
117
+ processes .put (testRunnerProcess , activityBlockName );
95
118
96
119
redirectStreamToLogger (testRunnerProcess .getInputStream (), new RedirectionTarget () {
97
120
public void redirect (String s ) {
@@ -103,28 +126,56 @@ public void redirect(String s) {
103
126
logger .warning (s );
104
127
}
105
128
});
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 );
111
139
}
112
-
113
- logger .activityFinished (activityBlockName , DefaultMessagesInfo .BLOCK_TYPE_MODULE );
114
140
}
115
141
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
+ }
116
156
return status ;
117
- }
118
- catch (Exception e ) {
157
+ } catch (Exception e ) {
119
158
logger .message ("Failed to run tests" );
120
159
logger .exception (e );
121
160
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 ;
122
172
}
123
173
}
124
174
125
175
private interface RedirectionTarget {
126
176
void redirect (String s );
127
177
}
178
+
128
179
private void redirectStreamToLogger (final InputStream s , final RedirectionTarget target ) {
129
180
new Thread (new Runnable () {
130
181
public void run () {
@@ -141,7 +192,7 @@ private String getCommandLineFlags(String version) {
141
192
// This is quite crude at the moment, but does the job.
142
193
// TODO: Migrate this into RunnerVersion or similar
143
194
char majorVersion = version .charAt (0 );
144
- if (majorVersion == '1' )
195
+ if (majorVersion == '1' )
145
196
return "/teamcity" ;
146
197
return "-teamcity" ;
147
198
}
0 commit comments