diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 19ce3d1a4..2e817aaa3 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -33,12 +33,15 @@ use function array_merge; use function array_merge_recursive; use function array_shift; +use function array_unique; +use function array_values; use function assert; use function count; use function dirname; use function file_get_contents; use function max; use function realpath; +use function str_contains; use function str_starts_with; use function unlink; use function unserialize; @@ -107,6 +110,11 @@ final class WrapperRunner implements RunnerInterface /** @var non-empty-string[] */ private readonly array $parameters; + /** @var list|null */ + private ?array $stopOutcomeChars = null; + + private bool $stopRequested = false; + /** * The code coverage filter registry. */ @@ -210,10 +218,7 @@ private function assignAllPendingTests(): void $worker = $this->startWorker($token); } - if ( - $this->exitcode > 0 - && $this->options->configuration->stopOnFailure() - ) { + if ($this->shouldStop()) { $this->pending = []; } elseif (($pending = array_shift($this->pending)) !== null) { $worker->assign($pending); @@ -225,6 +230,80 @@ private function assignAllPendingTests(): void } } + private function shouldStop(): bool + { + if ($this->stopRequested) { + return true; + } + + $outcomes = $this->stopOutcomeChars(); + if ($outcomes === []) { + return false; + } + + foreach ($this->workers as $worker) { + $progress = file_get_contents($worker->progressFile->getPathname()); + if ($progress === false || $progress === '') { + continue; + } + + foreach ($outcomes as $outcome) { + if (str_contains($progress, $outcome)) { + return $this->stopRequested = true; + } + } + } + + return false; + } + + /** + * @return list + */ + private function stopOutcomeChars(): array + { + if ($this->stopOutcomeChars !== null) { + return $this->stopOutcomeChars; + } + + $configuration = $this->options->configuration; + $outcomes = []; + + if ($configuration->stopOnFailure() || $configuration->stopOnDefect()) { + $outcomes[] = 'F'; + } + + if ($configuration->stopOnError() || $configuration->stopOnDefect()) { + $outcomes[] = 'E'; + } + + if ($configuration->stopOnRisky() || $configuration->stopOnDefect()) { + $outcomes[] = 'R'; + } + + if ($configuration->stopOnIncomplete() || $configuration->stopOnDefect()) { + $outcomes[] = 'I'; + } + + if ($configuration->stopOnWarning()) { + $outcomes[] = 'W'; + } + + if ($configuration->stopOnNotice()) { + $outcomes[] = 'N'; + } + + if ($configuration->stopOnDeprecation()) { + $outcomes[] = 'D'; + } + + if ($configuration->stopOnSkipped()) { + $outcomes[] = 'S'; + } + + return $this->stopOutcomeChars = array_values(array_unique($outcomes)); + } + private function flushWorker(WrapperWorker $worker): void { $this->exitcode = max($this->exitcode, $worker->getExitCode()); diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 0e38f50f0..68bf017b6 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1908,6 +1908,23 @@ ✓ parallel ✓ a parallel test can extend another test with same name ✓ parallel reports invalid datasets as failures + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('fails') + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('errors') + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('is risky') + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('is incomplete') + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('warns') + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('notices') + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('deprecates') + ✓ parallel stop-on-* fixture runs every case without a stop flag with ('is skipped') + ✓ parallel honors --stop-on-* with dataset "failure" + ✓ parallel honors --stop-on-* with dataset "defect" + ✓ parallel honors --stop-on-* with dataset "error" + ✓ parallel honors --stop-on-* with dataset "risky" + ✓ parallel honors --stop-on-* with dataset "incomplete" + ✓ parallel honors --stop-on-* with dataset "warning" + ✓ parallel honors --stop-on-* with dataset "notice" + ✓ parallel honors --stop-on-* with dataset "deprecation" + ✓ parallel honors --stop-on-* with dataset "skipped" PASS Tests\Visual\ParallelNestedDatasets ✓ parallel loads nested datasets from nested directories @@ -1941,4 +1958,4 @@ ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1330 passed (3010 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1347 passed (3027 assertions) \ No newline at end of file diff --git a/tests/.tests/ParallelStopOn/HasDeprecationTest.php b/tests/.tests/ParallelStopOn/HasDeprecationTest.php new file mode 100644 index 000000000..47e031b06 --- /dev/null +++ b/tests/.tests/ParallelStopOn/HasDeprecationTest.php @@ -0,0 +1,7 @@ +toBeTrue(); +}); diff --git a/tests/.tests/ParallelStopOn/HasErrorTest.php b/tests/.tests/ParallelStopOn/HasErrorTest.php new file mode 100644 index 000000000..7c9d0ed5d --- /dev/null +++ b/tests/.tests/ParallelStopOn/HasErrorTest.php @@ -0,0 +1,5 @@ +assertTrue(false); diff --git a/tests/.tests/ParallelStopOn/HasIncompleteTest.php b/tests/.tests/ParallelStopOn/HasIncompleteTest.php new file mode 100644 index 000000000..9c9fff147 --- /dev/null +++ b/tests/.tests/ParallelStopOn/HasIncompleteTest.php @@ -0,0 +1,5 @@ +markTestIncomplete('work in progress'); +}); diff --git a/tests/.tests/ParallelStopOn/HasNoticeTest.php b/tests/.tests/ParallelStopOn/HasNoticeTest.php new file mode 100644 index 000000000..552f11b23 --- /dev/null +++ b/tests/.tests/ParallelStopOn/HasNoticeTest.php @@ -0,0 +1,7 @@ +toBeTrue(); +}); diff --git a/tests/.tests/ParallelStopOn/HasRiskyTest.php b/tests/.tests/ParallelStopOn/HasRiskyTest.php new file mode 100644 index 000000000..5519b1ad0 --- /dev/null +++ b/tests/.tests/ParallelStopOn/HasRiskyTest.php @@ -0,0 +1,5 @@ +skip('skipping'); diff --git a/tests/.tests/ParallelStopOn/HasWarningTest.php b/tests/.tests/ParallelStopOn/HasWarningTest.php new file mode 100644 index 000000000..8fae1020d --- /dev/null +++ b/tests/.tests/ParallelStopOn/HasWarningTest.php @@ -0,0 +1,7 @@ +toBeTrue(); +}); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing01Test.php b/tests/.tests/ParallelStopOn/Passing/Passing01Test.php new file mode 100644 index 000000000..8dc2e1fb5 --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing01Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing02Test.php b/tests/.tests/ParallelStopOn/Passing/Passing02Test.php new file mode 100644 index 000000000..86045d01e --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing02Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing03Test.php b/tests/.tests/ParallelStopOn/Passing/Passing03Test.php new file mode 100644 index 000000000..057c6070f --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing03Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing04Test.php b/tests/.tests/ParallelStopOn/Passing/Passing04Test.php new file mode 100644 index 000000000..883200ba6 --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing04Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing05Test.php b/tests/.tests/ParallelStopOn/Passing/Passing05Test.php new file mode 100644 index 000000000..8a050a111 --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing05Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing06Test.php b/tests/.tests/ParallelStopOn/Passing/Passing06Test.php new file mode 100644 index 000000000..ceef2a2fb --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing06Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing07Test.php b/tests/.tests/ParallelStopOn/Passing/Passing07Test.php new file mode 100644 index 000000000..1f0a6b0f6 --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing07Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing08Test.php b/tests/.tests/ParallelStopOn/Passing/Passing08Test.php new file mode 100644 index 000000000..bae06622c --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing08Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing09Test.php b/tests/.tests/ParallelStopOn/Passing/Passing09Test.php new file mode 100644 index 000000000..bcc6b5e14 --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing09Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/.tests/ParallelStopOn/Passing/Passing10Test.php b/tests/.tests/ParallelStopOn/Passing/Passing10Test.php new file mode 100644 index 000000000..edc06d046 --- /dev/null +++ b/tests/.tests/ParallelStopOn/Passing/Passing10Test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index d497eef3c..1fb444168 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -47,3 +47,33 @@ ->toContain('Tests: 1 failed, 1 passed (1 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); + +test('parallel stop-on-* fixture runs every case without a stop flag', function (string $testName) use ($run) { + $output = $run('--filter', "($testName|passes)", 'tests/.tests/ParallelStopOn'); + expect($output)->toContain('10 passed'); +})->with([ + 'fails', + 'errors', + 'is risky', + 'is incomplete', + 'warns', + 'notices', + 'deprecates', + 'is skipped', +])->skipOnWindows(); + +test('parallel honors --stop-on-*', function (string $flag, string $testName) use ($run) { + $output = $run('--filter', "($testName|passes)", $flag, 'tests/.tests/ParallelStopOn'); + preg_match('/(\d+) passed/', $output, $matches); + expect((int) ($matches[1] ?? 0))->toBeLessThan(10); +})->with([ + 'failure' => ['--stop-on-failure', 'fails'], + 'defect' => ['--stop-on-defect', 'fails'], + 'error' => ['--stop-on-error', 'errors'], + 'risky' => ['--stop-on-risky', 'is risky'], + 'incomplete' => ['--stop-on-incomplete', 'is incomplete'], + 'warning' => ['--stop-on-warning', 'warns'], + 'notice' => ['--stop-on-notice', 'notices'], + 'deprecation' => ['--stop-on-deprecation', 'deprecates'], + 'skipped' => ['--stop-on-skipped', 'is skipped'], +])->skipOnWindows();