5
5
import java .io .IOException ;
6
6
import java .io .InputStream ;
7
7
import java .io .InputStreamReader ;
8
+ import java .io .PrintWriter ;
9
+ import java .io .StringWriter ;
8
10
import java .util .HashMap ;
9
11
import java .util .List ;
10
12
import java .util .Map ;
13
+ import java .util .StringTokenizer ;
14
+ import java .util .concurrent .CountDownLatch ;
15
+ import java .util .concurrent .ExecutorService ;
16
+ import java .util .concurrent .Executors ;
17
+ import java .util .concurrent .Future ;
18
+ import java .util .concurrent .TimeUnit ;
19
+ import java .util .concurrent .atomic .AtomicLong ;
11
20
import javax .xml .parsers .ParserConfigurationException ;
12
21
import org .apache .maven .plugin .AbstractMojo ;
13
22
import org .apache .maven .plugin .MojoExecutionException ;
18
27
19
28
@ Mojo (name = "supertest" )
20
29
public class SuperTestMavenPlugin extends AbstractMojo {
30
+ // this is the max time to wait in seconds for process termination after the stdout read is
31
+ // finished or terminated
32
+ private static final int STDOUT_POST_READ_WAIT_TIMEOUT = 10 ;
33
+
34
+ private ExecutorService pool ;
21
35
22
36
@ Parameter (defaultValue = "${project}" , readonly = true )
23
37
MavenProject project ;
@@ -31,6 +45,10 @@ public class SuperTestMavenPlugin extends AbstractMojo {
31
45
@ Parameter (property = "retryRunCount" , readonly = true , defaultValue = "1" )
32
46
Integer retryRunCount ;
33
47
48
+ // in seconds
49
+ @ Parameter (property = "shellNoActivityTimeout" , readonly = true , defaultValue = "300" )
50
+ Integer shellNoActivityTimeout ;
51
+
34
52
public void execute () throws MojoExecutionException {
35
53
36
54
if (mvnTestOpts == null ) {
@@ -50,6 +68,8 @@ public void execute() throws MojoExecutionException {
50
68
final String artifactId = project .getArtifactId ();
51
69
final String groupId = project .getGroupId ();
52
70
71
+ pool = Executors .newFixedThreadPool (1 );
72
+
53
73
int exitCode ;
54
74
final String command = "mvn test " + buildProcessedMvnTestOpts (artifactId , groupId );
55
75
try {
@@ -81,6 +101,12 @@ public void execute() throws MojoExecutionException {
81
101
}
82
102
83
103
final String runCommand = createRerunCommand (classnameToTestcaseList );
104
+
105
+ // previous run exited with code > 0, but all tests were actually run successfully
106
+ if (runCommand == null ) {
107
+ return ;
108
+ }
109
+
84
110
final StringBuilder rerunCommand = new StringBuilder (runCommand );
85
111
rerunCommand .append (buildProcessedMvnTestOpts (artifactId , groupId ));
86
112
if (rerunProfile != null ) {
@@ -101,6 +127,8 @@ public void execute() throws MojoExecutionException {
101
127
}
102
128
}
103
129
130
+ pool .shutdown ();
131
+
104
132
if (exitCode != 0 ) {
105
133
System .exit (1 );
106
134
}
@@ -121,16 +149,89 @@ private StringBuilder buildProcessedMvnTestOpts(String artifactId, String groupI
121
149
public int runShellCommand (final String command , final String commandDescriptor )
122
150
throws IOException , InterruptedException {
123
151
getLog ().info ("Running " + command );
124
- Process proc = Runtime .getRuntime ().exec (command );
152
+ ProcessBuilder pb = new ProcessBuilder (getShellCommandAsArray (command ));
153
+ pb .redirectErrorStream (true );
154
+ Process proc = pb .start ();
155
+ readProcessStdOut (proc , commandDescriptor );
156
+
157
+ // we don't want to wait forever, if something breaks
158
+ boolean exited = proc .waitFor (STDOUT_POST_READ_WAIT_TIMEOUT , TimeUnit .SECONDS );
159
+
160
+ if (exited ) {
161
+ return proc .exitValue ();
162
+ } else {
163
+ proc .destroyForcibly ();
164
+ return 1 ;
165
+ }
166
+ }
167
+
168
+ private String [] getShellCommandAsArray (String command ) {
169
+ // this is what Runtime.getRuntime().exec(...) is doing internally
170
+ StringTokenizer tokenizer = new StringTokenizer (command );
171
+ String [] cmdArray = new String [tokenizer .countTokens ()];
172
+ for (int i = 0 ; tokenizer .hasMoreTokens (); i ++) {
173
+ cmdArray [i ] = tokenizer .nextToken ();
174
+ }
175
+
176
+ return cmdArray ;
177
+ }
178
+
179
+ private void readProcessStdOut (Process proc , String commandDescriptor ) {
125
180
InputStream inputStream = proc .getInputStream ();
126
181
InputStreamReader inputStreamReader = new InputStreamReader (inputStream );
127
182
BufferedReader bufferedReader = new BufferedReader (inputStreamReader );
128
- String line ;
129
- while ((line = bufferedReader .readLine ()) != null ) {
130
- getLog ().info (commandDescriptor + ": " + line );
183
+ AtomicLong lastOutputTime = new AtomicLong (System .currentTimeMillis ());
184
+ CountDownLatch countDownLatch = new CountDownLatch (1 );
185
+
186
+ Future <?> task = pool .submit (() -> {
187
+ String line ;
188
+
189
+ try {
190
+ while ((line = bufferedReader .readLine ()) != null ) {
191
+ getLog ().info (commandDescriptor + ": " + line );
192
+ lastOutputTime .set (System .currentTimeMillis ());
193
+ }
194
+ } catch (IOException e ) {
195
+ throw new RuntimeException (e );
196
+ }
197
+
198
+ // task has finished
199
+ countDownLatch .countDown ();
200
+ });
201
+
202
+ boolean isDone = task .isDone ();
203
+
204
+ while (!isDone && hasRecentShellActivity (lastOutputTime .get ())) {
205
+ try {
206
+ isDone = countDownLatch .await (shellNoActivityTimeout , TimeUnit .SECONDS );
207
+ } catch (InterruptedException e ) {
208
+ Thread .currentThread ().interrupt ();
209
+ }
210
+ }
211
+
212
+ try {
213
+ // task is either done or it timed out, no need to wait much
214
+ task .get (1 , TimeUnit .SECONDS );
215
+ } catch (Exception e ) {
216
+ if (e instanceof InterruptedException ) {
217
+ Thread .currentThread ().interrupt ();
218
+ }
219
+
220
+ getLog ().info (commandDescriptor + ": Read stdout error - " + getStackTrace (e ));
131
221
}
132
- proc .waitFor ();
133
- return proc .exitValue ();
222
+ }
223
+
224
+ private boolean hasRecentShellActivity (long lastTime ) {
225
+ return System .currentTimeMillis () - lastTime
226
+ < TimeUnit .SECONDS .toMillis (shellNoActivityTimeout );
227
+ }
228
+
229
+ private String getStackTrace (Exception e ) {
230
+ StringWriter stringWriter = new StringWriter ();
231
+ PrintWriter printWriter = new PrintWriter (stringWriter );
232
+ e .printStackTrace (printWriter );
233
+
234
+ return stringWriter .getBuffer ().toString ();
134
235
}
135
236
136
237
/**
@@ -151,13 +252,15 @@ public File[] getXmlFileList(File baseDir) {
151
252
* @return rerunCommand
152
253
*/
153
254
public String createRerunCommand (Map <String , List <String >> classnameToTestcaseList ) {
255
+ boolean hasTestsAppended = false ;
154
256
final StringBuilder retryRun = new StringBuilder ("mvn test" );
155
257
retryRun .append (" -Dtest=" );
156
258
// TODO: 04/02/2022 replace with Java 8 streams
157
259
for (String className : classnameToTestcaseList .keySet ()) {
158
260
List <String > failedTestCaseList = classnameToTestcaseList .get (className );
159
261
if (!failedTestCaseList .isEmpty ()) {
160
262
retryRun .append (className );
263
+ hasTestsAppended = true ;
161
264
if (failedTestCaseList .contains ("" )) {
162
265
retryRun .append ("," );
163
266
continue ;
@@ -173,6 +276,6 @@ public String createRerunCommand(Map<String, List<String>> classnameToTestcaseLi
173
276
}
174
277
}
175
278
}
176
- return retryRun .toString ();
279
+ return hasTestsAppended ? retryRun .toString () : null ;
177
280
}
178
281
}
0 commit comments