diff --git a/phpunit.xsd b/phpunit.xsd index cca0a3b412d..f9b3b2f174b 100644 --- a/phpunit.xsd +++ b/phpunit.xsd @@ -258,6 +258,7 @@ + diff --git a/src/Framework/TestResult.php b/src/Framework/TestResult.php index 4a5b1634ed0..5fcd1883340 100644 --- a/src/Framework/TestResult.php +++ b/src/Framework/TestResult.php @@ -139,6 +139,11 @@ class TestResult implements Countable */ protected $beStrictAboutResourceUsageDuringSmallTests = false; + /** + * @var int + */ + private $defaultTimeLimit = 0; + /** * @var bool */ @@ -639,8 +644,8 @@ public function run(Test $test): void try { if (!$test instanceof WarningTestCase && - $test->getSize() != \PHPUnit\Util\Test::UNKNOWN && $this->enforceTimeLimit && + ($this->defaultTimeLimit || $test->getSize() != \PHPUnit\Util\Test::UNKNOWN) && \extension_loaded('pcntl') && \class_exists(Invoker::class)) { switch ($test->getSize()) { case \PHPUnit\Util\Test::SMALL: @@ -656,6 +661,11 @@ public function run(Test $test): void case \PHPUnit\Util\Test::LARGE: $_timeout = $this->timeoutForLargeTests; + break; + + case \PHPUnit\Util\Test::UNKNOWN: + $_timeout = $this->defaultTimeLimit; + break; } @@ -1061,6 +1071,14 @@ public function wasSuccessful(): bool return empty($this->errors) && empty($this->failures) && empty($this->warnings); } + /** + * Sets the default timeout for tests + */ + public function setDefaultTimeLimit(int $timeout): void + { + $this->defaultTimeLimit = $timeout; + } + /** * Sets the timeout for small tests. */ diff --git a/src/TextUI/Command.php b/src/TextUI/Command.php index 24c96c5bc96..7a95420672d 100644 --- a/src/TextUI/Command.php +++ b/src/TextUI/Command.php @@ -84,6 +84,7 @@ class Command 'disallow-test-output' => null, 'disallow-resource-usage' => null, 'disallow-todo-tests' => null, + 'default-time-limit=' => null, 'enforce-time-limit' => null, 'exclude-group=' => null, 'filter=' => null, @@ -692,6 +693,11 @@ protected function handleArguments(array $argv): void break; + case '--default-time-limit': + $this->arguments['defaultTimeLimit'] = (int) $option[1]; + + break; + case '--enforce-time-limit': $this->arguments['enforceTimeLimit'] = true; @@ -1120,6 +1126,7 @@ protected function showHelp(): void --disallow-test-output Be strict about output during tests --disallow-resource-usage Be strict about resource usage during small tests --enforce-time-limit Enforce time limit based on test size + --default-time-limit= Timeout in seconds for tests without @small, @medium or @large --disallow-todo-tests Disallow @todo-annotated tests --process-isolation Run each test in a separate PHP process diff --git a/src/TextUI/TestRunner.php b/src/TextUI/TestRunner.php index 98a6276de37..a054725ad32 100644 --- a/src/TextUI/TestRunner.php +++ b/src/TextUI/TestRunner.php @@ -54,6 +54,7 @@ use SebastianBergmann\CodeCoverage\Report\Xml\Facade as XmlReport; use SebastianBergmann\Comparator\Comparator; use SebastianBergmann\Environment\Runtime; +use SebastianBergmann\Invoker\Invoker; /** * A TestRunner for the Command Line Interface (CLI) @@ -579,7 +580,18 @@ public function doRun(Test $suite, array $arguments = [], bool $exit = true): Te $result->beStrictAboutOutputDuringTests($arguments['disallowTestOutput']); $result->beStrictAboutTodoAnnotatedTests($arguments['disallowTodoAnnotatedTests']); $result->beStrictAboutResourceUsageDuringSmallTests($arguments['beStrictAboutResourceUsageDuringSmallTests']); + + if ($arguments['enforceTimeLimit'] === true) { + if (!\class_exists(Invoker::class)) { + $this->writeMessage('Error', 'Package phpunit/php-invoker is required for enforcing time limits'); + } + + if (!\extension_loaded('pcntl') || \strpos(\ini_get('disable_functions'), 'pcntl') !== false) { + $this->writeMessage('Error', 'PHP extension pcntl is required for enforcing time limits'); + } + } $result->enforceTimeLimit($arguments['enforceTimeLimit']); + $result->setDefaultTimeLimit($arguments['defaultTimeLimit']); $result->setTimeoutForSmallTests($arguments['timeoutForSmallTests']); $result->setTimeoutForMediumTests($arguments['timeoutForMediumTests']); $result->setTimeoutForLargeTests($arguments['timeoutForLargeTests']); @@ -942,6 +954,10 @@ protected function handleConfiguration(array &$arguments): void $arguments['disallowTestOutput'] = $phpunitConfiguration['disallowTestOutput']; } + if (isset($phpunitConfiguration['defaultTimeLimit']) && !isset($arguments['defaultTimeLimit'])) { + $arguments['defaultTimeLimit'] = $phpunitConfiguration['defaultTimeLimit']; + } + if (isset($phpunitConfiguration['enforceTimeLimit']) && !isset($arguments['enforceTimeLimit'])) { $arguments['enforceTimeLimit'] = $phpunitConfiguration['enforceTimeLimit']; } @@ -1179,6 +1195,7 @@ protected function handleConfiguration(array &$arguments): void $arguments['crap4jThreshold'] = $arguments['crap4jThreshold'] ?? 30; $arguments['disallowTestOutput'] = $arguments['disallowTestOutput'] ?? false; $arguments['disallowTodoAnnotatedTests'] = $arguments['disallowTodoAnnotatedTests'] ?? false; + $arguments['defaultTimeLimit'] = $arguments['defaultTimeLimit'] ?? 0; $arguments['enforceTimeLimit'] = $arguments['enforceTimeLimit'] ?? false; $arguments['excludeGroups'] = $arguments['excludeGroups'] ?? []; $arguments['failOnRisky'] = $arguments['failOnRisky'] ?? false; diff --git a/src/Util/Configuration.php b/src/Util/Configuration.php index ad70571c888..0a184303936 100644 --- a/src/Util/Configuration.php +++ b/src/Util/Configuration.php @@ -59,6 +59,7 @@ * beStrictAboutResourceUsageDuringSmallTests="false" * beStrictAboutTestsThatDoNotTestAnything="false" * beStrictAboutTodoAnnotatedTests="false" + * defaultTimeLimit="0" * enforceTimeLimit="false" * ignoreDeprecatedCodeUnitsFromCodeCoverage="false" * timeoutForSmallTests="1" @@ -855,6 +856,13 @@ public function getPHPUnitConfiguration(): array ); } + if ($root->hasAttribute('defaultTimeLimit')) { + $result['defaultTimeLimit'] = $this->getInteger( + (string) $root->getAttribute('defaultTimeLimit'), + 1 + ); + } + if ($root->hasAttribute('enforceTimeLimit')) { $result['enforceTimeLimit'] = $this->getBoolean( (string) $root->getAttribute('enforceTimeLimit'), diff --git a/tests/Regression/GitHub/2085/Issue2085Test.php b/tests/Regression/GitHub/2085/Issue2085Test.php new file mode 100644 index 00000000000..c97efe2621d --- /dev/null +++ b/tests/Regression/GitHub/2085/Issue2085Test.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +use PHPUnit\Framework\TestCase; + +class Issue2085Test extends TestCase +{ + public function testShouldAbortSlowTestByEnforcingTimeLimit(): void + { + $this->assertTrue(true); + \sleep(1.2); + $this->assertTrue(true); + } +} diff --git a/tests/Regression/GitHub/2085/configuration_enforce_time_limit_options.xml b/tests/Regression/GitHub/2085/configuration_enforce_time_limit_options.xml new file mode 100644 index 00000000000..609ca4a505d --- /dev/null +++ b/tests/Regression/GitHub/2085/configuration_enforce_time_limit_options.xml @@ -0,0 +1,2 @@ + + diff --git a/tests/Regression/GitHub/2085/enforce-time-limit-options-via-config-without-invoker.phpt b/tests/Regression/GitHub/2085/enforce-time-limit-options-via-config-without-invoker.phpt new file mode 100644 index 00000000000..5dc14d8e71d --- /dev/null +++ b/tests/Regression/GitHub/2085/enforce-time-limit-options-via-config-without-invoker.phpt @@ -0,0 +1,31 @@ +--TEST-- +Test XML config enforceTimeLimit, defaultTimeLimit without php-invoker, with pcntl +--SKIPIF-- + Timeout in seconds for tests without @small, @medium or @large --disallow-todo-tests Disallow @todo-annotated tests --process-isolation Run each test in a separate PHP process diff --git a/tests/end-to-end/help2.phpt b/tests/end-to-end/help2.phpt index 7e9b5d06fe9..05c9688f226 100644 --- a/tests/end-to-end/help2.phpt +++ b/tests/end-to-end/help2.phpt @@ -57,6 +57,7 @@ Test Execution Options: --disallow-test-output Be strict about output during tests --disallow-resource-usage Be strict about resource usage during small tests --enforce-time-limit Enforce time limit based on test size + --default-time-limit= Timeout in seconds for tests without @small, @medium or @large --disallow-todo-tests Disallow @todo-annotated tests --process-isolation Run each test in a separate PHP process diff --git a/tests/unit/Util/ConfigurationTest.php b/tests/unit/Util/ConfigurationTest.php index 92bffa2d5e8..95d8652ebff 100644 --- a/tests/unit/Util/ConfigurationTest.php +++ b/tests/unit/Util/ConfigurationTest.php @@ -471,6 +471,7 @@ public function testPHPUnitConfigurationIsReadCorrectly(): void 'reportUselessTests' => false, 'strictCoverage' => false, 'disallowTestOutput' => false, + 'defaultTimeLimit' => 123, 'enforceTimeLimit' => false, 'extensionsDirectory' => '/tmp', 'printerClass' => 'PHPUnit\TextUI\ResultPrinter',