|
1 | 1 | import java.io.BufferedReader; |
2 | 2 | import java.io.InputStreamReader; |
3 | | -// Import Duration |
| 3 | +import java.time.Duration; |
4 | 4 | import java.util.Arrays; |
5 | 5 | import java.util.Collection; |
6 | 6 | import java.util.List; |
| 7 | +import java.util.concurrent.Callable; |
| 8 | +import java.util.concurrent.ExecutionException; |
| 9 | +import java.util.concurrent.ExecutorService; |
| 10 | +import java.util.concurrent.Executors; |
| 11 | +import java.util.concurrent.Future; |
| 12 | +import java.util.concurrent.TimeUnit; |
| 13 | +import java.util.concurrent.TimeoutException; |
7 | 14 | import java.util.stream.Collectors; |
8 | 15 | import org.junit.jupiter.api.Assertions; |
9 | 16 | import org.junit.jupiter.api.DynamicTest; |
10 | 17 | 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 |
14 | 18 |
|
15 | | -// Enable concurrent execution for tests in this class |
16 | | -@Execution(ExecutionMode.CONCURRENT) |
17 | 19 | public class MavenTest { |
18 | 20 |
|
19 | 21 | private static final List<String> PROBLEMS = Arrays.asList( |
| 22 | + // ... (Your existing long list of problems) ... |
20 | 23 | "p100", |
21 | 24 | "p102", |
22 | 25 | "p104", |
@@ -385,113 +388,182 @@ public class MavenTest { |
385 | 388 | "p1673", |
386 | 389 | "p190", |
387 | 390 | "p191", |
388 | | - "p195"); |
| 391 | + "p195" |
| 392 | + // ... (End of your existing list) ... |
| 393 | + ); |
| 394 | + |
| 395 | + // Define the number of threads for the ExecutorService |
| 396 | + private static final int MAX_THREADS = 10; |
| 397 | + // Define the timeout for each test |
| 398 | + private static final Duration TEST_TIMEOUT = Duration.ofSeconds(3); |
389 | 399 |
|
390 | 400 | @TestFactory |
391 | | - @Timeout(value = 3, unit = java.util.concurrent.TimeUnit.SECONDS) // Apply a 3-second timeout to each dynamic test |
392 | 401 | Collection<DynamicTest> runMavenExecTests() { |
393 | | - return PROBLEMS.stream() |
394 | | - .map(problem -> DynamicTest.dynamicTest("Test problem: " + problem, () -> { |
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. |
399 | | - |
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); |
| 402 | + // Create a fixed-size thread pool |
| 403 | + final ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS); |
408 | 404 |
|
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); |
| 405 | + // A list to hold the Futures of each submitted task |
| 406 | + List<Future<TestResult>> futures = PROBLEMS.stream() |
| 407 | + .map(problem -> { |
| 408 | + // Create a Callable for each problem |
| 409 | + Callable<TestResult> task = () -> { |
| 410 | + Thread.currentThread().setName("Problem-Runner-" + problem); // Name thread for better logging |
| 411 | + String command = String.format("mvn exec:exec -Dproblem=%s", problem); |
| 412 | + System.out.println(Thread.currentThread().getName() + ": Executing command for " + problem |
| 413 | + + ": " + command); |
415 | 414 |
|
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 |
422 | | - } |
| 415 | + Process process; |
| 416 | + try { |
| 417 | + process = Runtime.getRuntime().exec(command); |
| 418 | + } catch (Exception e) { |
| 419 | + // If process execution itself fails |
| 420 | + return new TestResult( |
| 421 | + problem, false, "", "Failed to execute command: " + e.getMessage(), e); |
| 422 | + } |
423 | 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(); |
427 | | - StringBuilder errorOutput = new StringBuilder(); |
| 424 | + StringBuilder output = new StringBuilder(); |
| 425 | + StringBuilder errorOutput = new StringBuilder(); |
428 | 426 |
|
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"); |
| 427 | + // Use separate threads to consume streams to prevent deadlock |
| 428 | + Thread outputGobbler = new Thread(() -> { |
| 429 | + try (BufferedReader reader = |
| 430 | + new BufferedReader(new InputStreamReader(process.getInputStream()))) { |
| 431 | + String line; |
| 432 | + while ((line = reader.readLine()) != null) { |
| 433 | + output.append(line).append("\n"); |
| 434 | + } |
| 435 | + } catch (Exception e) { |
| 436 | + System.err.println(Thread.currentThread().getName() + ": Error reading output for " |
| 437 | + + problem + ": " + e.getMessage()); |
437 | 438 | } |
438 | | - } catch (Exception e) { |
439 | | - System.err.println("Error reading output for " + problem + ": " + e.getMessage()); |
440 | | - } |
441 | | - }); |
| 439 | + }); |
442 | 440 |
|
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"); |
| 441 | + Thread errorGobbler = new Thread(() -> { |
| 442 | + try (BufferedReader errorReader = |
| 443 | + new BufferedReader(new InputStreamReader(process.getErrorStream()))) { |
| 444 | + String line; |
| 445 | + while ((line = errorReader.readLine()) != null) { |
| 446 | + errorOutput.append(line).append("\n"); |
| 447 | + } |
| 448 | + } catch (Exception e) { |
| 449 | + System.err.println(Thread.currentThread().getName() |
| 450 | + + ": Error reading error output for " + problem + ": " + e.getMessage()); |
449 | 451 | } |
| 452 | + }); |
| 453 | + |
| 454 | + outputGobbler.start(); |
| 455 | + errorGobbler.start(); |
| 456 | + |
| 457 | + int exitCode; |
| 458 | + try { |
| 459 | + exitCode = process.waitFor(); |
| 460 | + outputGobbler.join(); // Ensure all output is consumed |
| 461 | + errorGobbler.join(); // Ensure all error output is consumed |
| 462 | + } catch (InterruptedException e) { |
| 463 | + process.destroyForcibly(); // Ensure subprocess is terminated if interrupted |
| 464 | + outputGobbler.join(100); // Give gobblers a moment, but don't hang |
| 465 | + errorGobbler.join(100); |
| 466 | + Thread.currentThread().interrupt(); // Restore interrupted status |
| 467 | + return new TestResult( |
| 468 | + problem, false, output.toString(), "Test interrupted (likely timed out)", e); |
450 | 469 | } catch (Exception e) { |
451 | | - System.err.println("Error reading error output for " + problem + ": " + e.getMessage()); |
| 470 | + return new TestResult( |
| 471 | + problem, |
| 472 | + false, |
| 473 | + output.toString(), |
| 474 | + "Error waiting for process: " + e.getMessage(), |
| 475 | + e); |
452 | 476 | } |
453 | | - }); |
454 | 477 |
|
455 | | - outputGobbler.start(); |
456 | | - errorGobbler.start(); |
| 478 | + boolean success = (exitCode == 0); |
| 479 | + return new TestResult(problem, success, output.toString(), errorOutput.toString(), null); |
| 480 | + }; |
| 481 | + return executor.submit(task); // Submit the task to the executor |
| 482 | + }) |
| 483 | + .collect(Collectors.toList()); |
457 | 484 |
|
458 | | - int exitCode; |
| 485 | + // Create DynamicTests to check the results of the submitted tasks |
| 486 | + Collection<DynamicTest> dynamicTests = futures.stream() |
| 487 | + .map(future -> DynamicTest.dynamicTest("Test problem: " + future.toString(), () -> { |
| 488 | + TestResult result = null; |
459 | 489 | 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 --- |
| 490 | + // Wait for each task to complete with the defined timeout |
| 491 | + result = future.get(TEST_TIMEOUT.toSeconds(), TimeUnit.SECONDS); |
481 | 492 |
|
482 | | - System.out.println(Thread.currentThread().getName() + ": Command output for " + problem + ":\n" |
483 | | - + output.toString()); |
484 | | - if (errorOutput.length() > 0) { |
485 | | - System.err.println(Thread.currentThread().getName() + ": Command error for " + problem + ":\n" |
486 | | - + errorOutput.toString()); |
487 | | - } |
| 493 | + System.out.println("Test " + result.problemName + " completed. Output:\n" + result.output); |
| 494 | + if (!result.errorOutput.isEmpty()) { |
| 495 | + System.err.println("Test " + result.problemName + " error output:\n" + result.errorOutput); |
| 496 | + } |
488 | 497 |
|
489 | | - Assertions.assertEquals( |
490 | | - 0, |
491 | | - exitCode, |
492 | | - "Maven command failed for problem: " + problem + "\nError output:\n" |
493 | | - + errorOutput.toString()); |
| 498 | + Assertions.assertTrue( |
| 499 | + result.success, |
| 500 | + "Maven command failed for problem: " + result.problemName + "\nError output:\n" |
| 501 | + + result.errorOutput); |
| 502 | + |
| 503 | + } catch (TimeoutException e) { |
| 504 | + // This handles the case where the Callable itself exceeds the timeout |
| 505 | + future.cancel(true); // Attempt to interrupt the running task |
| 506 | + Assertions.fail( |
| 507 | + "Test for problem " + (result != null ? result.problemName : "unknown") |
| 508 | + + " timed out after " + TEST_TIMEOUT.toSeconds() + " seconds.", |
| 509 | + e); |
| 510 | + } catch (InterruptedException e) { |
| 511 | + Thread.currentThread().interrupt(); // Restore interrupt status |
| 512 | + Assertions.fail( |
| 513 | + "Test for problem " + (result != null ? result.problemName : "unknown") |
| 514 | + + " was interrupted.", |
| 515 | + e); |
| 516 | + } catch (ExecutionException e) { |
| 517 | + // The actual exception thrown by the Callable is wrapped here |
| 518 | + Throwable cause = e.getCause(); |
| 519 | + Assertions.fail( |
| 520 | + "An error occurred during execution for problem " |
| 521 | + + (result != null ? result.problemName : "unknown") + ": " + cause.getMessage(), |
| 522 | + cause); |
| 523 | + } |
494 | 524 | })) |
495 | 525 | .collect(Collectors.toList()); |
| 526 | + |
| 527 | + // Crucial: Shut down the executor after all tests have been processed |
| 528 | + // For a @TestFactory, this is a bit tricky as the tests are returned, not run immediately. |
| 529 | + // The safest place to shut down is after collecting all dynamic tests, or in an @AfterAll method. |
| 530 | + // However, @AfterAll needs the ExecutorService to be static. |
| 531 | + // For simple test runs, `shutdownNow()` is often acceptable here. |
| 532 | + executor.shutdown(); |
| 533 | + try { |
| 534 | + // Wait for existing tasks to terminate |
| 535 | + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { |
| 536 | + executor.shutdownNow(); // Forcefully terminate if not done in time |
| 537 | + } |
| 538 | + } catch (InterruptedException e) { |
| 539 | + executor.shutdownNow(); |
| 540 | + Thread.currentThread().interrupt(); // Restore interrupt status |
| 541 | + } |
| 542 | + |
| 543 | + return dynamicTests; |
| 544 | + } |
| 545 | + |
| 546 | + // A simple record/class to encapsulate test results |
| 547 | + private static class TestResult { |
| 548 | + String problemName; |
| 549 | + boolean success; |
| 550 | + String output; |
| 551 | + String errorOutput; |
| 552 | + Throwable exception; // To store any exception from the callable |
| 553 | + |
| 554 | + // Primary constructor |
| 555 | + public TestResult(String problemName, boolean success, String output, String errorOutput, Throwable exception) { |
| 556 | + this.problemName = problemName; |
| 557 | + this.success = success; |
| 558 | + this.output = output; |
| 559 | + this.errorOutput = errorOutput; |
| 560 | + this.exception = exception; |
| 561 | + } |
| 562 | + |
| 563 | + @Override |
| 564 | + public String toString() { |
| 565 | + // Used by DynamicTest.dynamicTest() to name the test |
| 566 | + return problemName; |
| 567 | + } |
496 | 568 | } |
497 | 569 | } |
0 commit comments