|
1 | 1 | import java.io.BufferedReader; |
2 | 2 | import java.io.InputStreamReader; |
| 3 | +// Import Duration |
3 | 4 | import java.util.Arrays; |
4 | 5 | import java.util.Collection; |
5 | 6 | import java.util.List; |
6 | 7 | import java.util.stream.Collectors; |
7 | 8 | import org.junit.jupiter.api.Assertions; |
8 | 9 | import org.junit.jupiter.api.DynamicTest; |
9 | 10 | 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 |
10 | 14 |
|
| 15 | +// Enable concurrent execution for tests in this class |
| 16 | +@Execution(ExecutionMode.CONCURRENT) |
11 | 17 | public class MavenTest { |
12 | 18 |
|
13 | 19 | private static final List<String> PROBLEMS = Arrays.asList( |
@@ -382,36 +388,102 @@ public class MavenTest { |
382 | 388 | "p195"); |
383 | 389 |
|
384 | 390 | @TestFactory |
| 391 | + @Timeout(value = 3, unit = java.util.concurrent.TimeUnit.SECONDS) // Apply a 3-second timeout to each dynamic test |
385 | 392 | Collection<DynamicTest> runMavenExecTests() { |
386 | 393 | return PROBLEMS.stream() |
387 | 394 | .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. |
390 | 399 |
|
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); |
392 | 408 |
|
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); |
396 | 415 |
|
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 |
401 | 422 | } |
402 | | - reader.close(); |
403 | 423 |
|
| 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(); |
404 | 427 | StringBuilder errorOutput = new StringBuilder(); |
405 | | - while ((line = errorReader.readLine()) != null) { |
406 | | - errorOutput.append(line).append("\n"); |
407 | | - } |
408 | | - errorReader.close(); |
409 | 428 |
|
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 --- |
411 | 481 |
|
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()); |
413 | 484 | 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()); |
415 | 487 | } |
416 | 488 |
|
417 | 489 | Assertions.assertEquals( |
|
0 commit comments