Skip to content

Commit d1c3e44

Browse files
authored
Merge pull request #22 from CleverTap/run-shell-read-stderr
Enhance mvn test execution to prevent process being stuck
2 parents 30ceb33 + ce89a8e commit d1c3e44

File tree

2 files changed

+111
-8
lines changed

2 files changed

+111
-8
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<groupId>com.clevertap</groupId>
88
<artifactId>supertest-maven-plugin</artifactId>
99
<packaging>maven-plugin</packaging>
10-
<version>1.10</version>
10+
<version>1.11</version>
1111
<description>A wrapper for Maven's Surefire Plugin, with advanced re-run capabilities.
1212
</description>
1313
<name>supertest-maven-plugin</name>

src/main/java/com/clevertap/maven/plugins/supertest/SuperTestMavenPlugin.java

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@
55
import java.io.IOException;
66
import java.io.InputStream;
77
import java.io.InputStreamReader;
8+
import java.io.PrintWriter;
9+
import java.io.StringWriter;
810
import java.util.HashMap;
911
import java.util.List;
1012
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;
1120
import javax.xml.parsers.ParserConfigurationException;
1221
import org.apache.maven.plugin.AbstractMojo;
1322
import org.apache.maven.plugin.MojoExecutionException;
@@ -18,6 +27,11 @@
1827

1928
@Mojo(name = "supertest")
2029
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;
2135

2236
@Parameter(defaultValue = "${project}", readonly = true)
2337
MavenProject project;
@@ -31,6 +45,10 @@ public class SuperTestMavenPlugin extends AbstractMojo {
3145
@Parameter(property = "retryRunCount", readonly = true, defaultValue = "1")
3246
Integer retryRunCount;
3347

48+
// in seconds
49+
@Parameter(property = "shellNoActivityTimeout", readonly = true, defaultValue = "300")
50+
Integer shellNoActivityTimeout;
51+
3452
public void execute() throws MojoExecutionException {
3553

3654
if (mvnTestOpts == null) {
@@ -50,6 +68,8 @@ public void execute() throws MojoExecutionException {
5068
final String artifactId = project.getArtifactId();
5169
final String groupId = project.getGroupId();
5270

71+
pool = Executors.newFixedThreadPool(1);
72+
5373
int exitCode;
5474
final String command = "mvn test " + buildProcessedMvnTestOpts(artifactId, groupId);
5575
try {
@@ -81,6 +101,12 @@ public void execute() throws MojoExecutionException {
81101
}
82102

83103
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+
84110
final StringBuilder rerunCommand = new StringBuilder(runCommand);
85111
rerunCommand.append(buildProcessedMvnTestOpts(artifactId, groupId));
86112
if (rerunProfile != null) {
@@ -101,6 +127,8 @@ public void execute() throws MojoExecutionException {
101127
}
102128
}
103129

130+
pool.shutdown();
131+
104132
if (exitCode != 0) {
105133
System.exit(1);
106134
}
@@ -121,16 +149,89 @@ private StringBuilder buildProcessedMvnTestOpts(String artifactId, String groupI
121149
public int runShellCommand(final String command, final String commandDescriptor)
122150
throws IOException, InterruptedException {
123151
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) {
125180
InputStream inputStream = proc.getInputStream();
126181
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
127182
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));
131221
}
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();
134235
}
135236

136237
/**
@@ -151,13 +252,15 @@ public File[] getXmlFileList(File baseDir) {
151252
* @return rerunCommand
152253
*/
153254
public String createRerunCommand(Map<String, List<String>> classnameToTestcaseList) {
255+
boolean hasTestsAppended = false;
154256
final StringBuilder retryRun = new StringBuilder("mvn test");
155257
retryRun.append(" -Dtest=");
156258
// TODO: 04/02/2022 replace with Java 8 streams
157259
for (String className : classnameToTestcaseList.keySet()) {
158260
List<String> failedTestCaseList = classnameToTestcaseList.get(className);
159261
if (!failedTestCaseList.isEmpty()) {
160262
retryRun.append(className);
263+
hasTestsAppended = true;
161264
if(failedTestCaseList.contains("")) {
162265
retryRun.append(",");
163266
continue;
@@ -173,6 +276,6 @@ public String createRerunCommand(Map<String, List<String>> classnameToTestcaseLi
173276
}
174277
}
175278
}
176-
return retryRun.toString();
279+
return hasTestsAppended ? retryRun.toString() : null;
177280
}
178281
}

0 commit comments

Comments
 (0)