Skip to content

Commit b2769f3

Browse files
committed
feat(test): enable concurrent test execution with timeouts
1 parent a0f7ba7 commit b2769f3

File tree

2 files changed

+109
-18
lines changed

2 files changed

+109
-18
lines changed

src/test/java/MavenTest.java

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import java.io.BufferedReader;
22
import java.io.InputStreamReader;
3+
// Import Duration
34
import java.util.Arrays;
45
import java.util.Collection;
56
import java.util.List;
67
import java.util.stream.Collectors;
78
import org.junit.jupiter.api.Assertions;
89
import org.junit.jupiter.api.DynamicTest;
910
import org.junit.jupiter.api.TestFactory;
11+
import org.junit.jupiter.api.Timeout; // Import Timeout
12+
import org.junit.jupiter.api.parallel.Execution; // Import Execution
13+
import org.junit.jupiter.api.parallel.ExecutionMode; // Import ExecutionMode
1014

15+
// Enable concurrent execution for tests in this class
16+
@Execution(ExecutionMode.CONCURRENT)
1117
public class MavenTest {
1218

1319
private static final List<String> PROBLEMS = Arrays.asList(
@@ -382,36 +388,102 @@ public class MavenTest {
382388
"p195");
383389

384390
@TestFactory
391+
@Timeout(value = 3, unit = java.util.concurrent.TimeUnit.SECONDS) // Apply a 3-second timeout to each dynamic test
385392
Collection<DynamicTest> runMavenExecTests() {
386393
return PROBLEMS.stream()
387394
.map(problem -> DynamicTest.dynamicTest("Test problem: " + problem, () -> {
388-
String command = String.format("mvn exec:exec -Dproblem=%s", problem);
389-
System.out.println("Executing command: " + command);
395+
// This command needs to directly execute the Java Main class,
396+
// NOT "mvn exec:exec" if you want full parallelization
397+
// and proper timeout handling by JUnit 5.
398+
// The `exec-maven-plugin` creates its own process.
390399

391-
Process process = Runtime.getRuntime().exec(command);
400+
// You need to ensure the Main class can be run directly
401+
// and can handle input redirection if needed.
402+
// Example: /opt/homebrew/Cellar/openjdk/24.0.1/bin/java -cp ...
403+
// If you compile your project, the classes will be in target/classes.
404+
// String javaCommand = String.format("/opt/homebrew/Cellar/openjdk/24.0.1/bin/java -cp
405+
// target/classes com.lzw.solutions.uva.%s.Main < src/main/resources/uva/%s/1.in", problem,
406+
// problem);
407+
// System.out.println("Executing command: " + javaCommand);
392408

393-
// Capture output and error streams
394-
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
395-
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
409+
// For now, let's stick to your `mvn exec:exec` command, but be aware
410+
// it might not be the most efficient for JUnit's parallel execution.
411+
// The timeout here will apply to the *entire* 'mvn exec:exec' process.
412+
String command = String.format("mvn exec:exec -Dproblem=%s", problem);
413+
System.out.println(
414+
Thread.currentThread().getName() + ": Executing command for " + problem + ": " + command);
396415

397-
StringBuilder output = new StringBuilder();
398-
String line;
399-
while ((line = reader.readLine()) != null) {
400-
output.append(line).append("\n");
416+
Process process;
417+
try {
418+
process = Runtime.getRuntime().exec(command);
419+
} catch (Exception e) {
420+
Assertions.fail("Failed to execute command for problem " + problem + ": " + e.getMessage());
421+
return; // Exit if process creation fails
401422
}
402-
reader.close();
403423

424+
// --- Start: Capture output and error streams (with a small buffer size for efficiency) ---
425+
// Using try-with-resources for automatic closing of readers
426+
StringBuilder output = new StringBuilder();
404427
StringBuilder errorOutput = new StringBuilder();
405-
while ((line = errorReader.readLine()) != null) {
406-
errorOutput.append(line).append("\n");
407-
}
408-
errorReader.close();
409428

410-
int exitCode = process.waitFor();
429+
// Using separate threads to consume streams to prevent deadlock
430+
// if process produces a lot of output on both streams
431+
Thread outputGobbler = new Thread(() -> {
432+
try (BufferedReader reader =
433+
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
434+
String line;
435+
while ((line = reader.readLine()) != null) {
436+
output.append(line).append("\n");
437+
}
438+
} catch (Exception e) {
439+
System.err.println("Error reading output for " + problem + ": " + e.getMessage());
440+
}
441+
});
442+
443+
Thread errorGobbler = new Thread(() -> {
444+
try (BufferedReader errorReader =
445+
new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
446+
String line;
447+
while ((line = errorReader.readLine()) != null) {
448+
errorOutput.append(line).append("\n");
449+
}
450+
} catch (Exception e) {
451+
System.err.println("Error reading error output for " + problem + ": " + e.getMessage());
452+
}
453+
});
454+
455+
outputGobbler.start();
456+
errorGobbler.start();
457+
458+
int exitCode;
459+
try {
460+
// Wait for the process to complete or timeout
461+
// This timeout is managed by JUnit's @Timeout annotation
462+
// and applies to the entire lambda body.
463+
exitCode = process.waitFor();
464+
outputGobbler.join(); // Ensure all output is consumed
465+
errorGobbler.join(); // Ensure all error output is consumed
466+
} catch (InterruptedException e) {
467+
// This block is executed if the JUnit timeout is triggered
468+
process.destroyForcibly(); // Terminate the process if interrupted
469+
outputGobbler.join(100); // Give gobblers a moment to finish, but don't wait forever
470+
errorGobbler.join(100);
471+
System.err.println(Thread.currentThread().getName() + ": Process for " + problem
472+
+ " was interrupted/timed out. Output:\n" + output.toString() + "\nError:\n"
473+
+ errorOutput.toString());
474+
throw new org.junit.platform.commons.JUnitException(
475+
"Test for problem " + problem + " timed out after 3 seconds.", e);
476+
} catch (Exception e) {
477+
Assertions.fail("Error waiting for process for problem " + problem + ": " + e.getMessage());
478+
return;
479+
}
480+
// --- End: Capture output and error streams ---
411481

412-
System.out.println("Command output for " + problem + ":\n" + output.toString());
482+
System.out.println(Thread.currentThread().getName() + ": Command output for " + problem + ":\n"
483+
+ output.toString());
413484
if (errorOutput.length() > 0) {
414-
System.err.println("Command error for " + problem + ":\n" + errorOutput.toString());
485+
System.err.println(Thread.currentThread().getName() + ": Command error for " + problem + ":\n"
486+
+ errorOutput.toString());
415487
}
416488

417489
Assertions.assertEquals(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Enable parallel execution
2+
junit.jupiter.execution.parallel.enabled = true
3+
4+
# Choose the execution mode:
5+
# SAME_THREAD: Runs everything in the same thread (default)
6+
# CONCURRENT: Runs @Test, @RepeatedTest, @ParameterizedTest, @DynamicTest in parallel
7+
junit.jupiter.execution.parallel.mode.default = concurrent
8+
9+
# Configure the thread pool
10+
# fixed: a fixed number of threads
11+
# dynamic: a number of threads equal to available processors (Runtime.getRuntime().availableProcessors())
12+
junit.jupiter.execution.parallel.config.strategy = fixed
13+
14+
# If strategy is 'fixed', specify the number of threads
15+
# Set to 10 for your requirement
16+
junit.jupiter.execution.parallel.config.fixed.parallelism = 10
17+
18+
# If you use 'dynamic', you can still scale it, e.g., 2 * available processors
19+
# junit.jupiter.execution.parallel.config.factor = 2

0 commit comments

Comments
 (0)