Revision 148 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?php declare(strict_types=1);/** This file is part of PHPUnit.** (c) Sebastian Bergmann <sebastian@phpunit.de>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace PHPUnit\Framework;use const PHP_EOL;use function class_exists;use function count;use function extension_loaded;use function function_exists;use function get_class;use function sprintf;use function xdebug_get_monitored_functions;use function xdebug_is_debugger_active;use function xdebug_start_function_monitor;use function xdebug_stop_function_monitor;use AssertionError;use Countable;use Error;use PHPUnit\Util\ErrorHandler;use PHPUnit\Util\ExcludeList;use PHPUnit\Util\Printer;use PHPUnit\Util\Test as TestUtil;use ReflectionClass;use ReflectionException;use SebastianBergmann\CodeCoverage\CodeCoverage;use SebastianBergmann\CodeCoverage\Exception as OriginalCodeCoverageException;use SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException;use SebastianBergmann\Invoker\Invoker;use SebastianBergmann\Invoker\TimeoutException;use SebastianBergmann\ResourceOperations\ResourceOperations;use SebastianBergmann\Timer\Timer;use Throwable;/*** @internal This class is not covered by the backward compatibility promise for PHPUnit*/final class TestResult implements Countable{/*** @var array*/private $passed = [];/*** @var array<string>*/private $passedTestClasses = [];/*** @var bool*/private $currentTestSuiteFailed = false;/*** @var TestFailure[]*/private $errors = [];/*** @var TestFailure[]*/private $failures = [];/*** @var TestFailure[]*/private $warnings = [];/*** @var TestFailure[]*/private $notImplemented = [];/*** @var TestFailure[]*/private $risky = [];/*** @var TestFailure[]*/private $skipped = [];/*** @deprecated Use the `TestHook` interfaces instead** @var TestListener[]*/private $listeners = [];/*** @var int*/private $runTests = 0;/*** @var float*/private $time = 0;/*** Code Coverage information.** @var CodeCoverage*/private $codeCoverage;/*** @var bool*/private $convertDeprecationsToExceptions = false;/*** @var bool*/private $convertErrorsToExceptions = true;/*** @var bool*/private $convertNoticesToExceptions = true;/*** @var bool*/private $convertWarningsToExceptions = true;/*** @var bool*/private $stop = false;/*** @var bool*/private $stopOnError = false;/*** @var bool*/private $stopOnFailure = false;/*** @var bool*/private $stopOnWarning = false;/*** @var bool*/private $beStrictAboutTestsThatDoNotTestAnything = true;/*** @var bool*/private $beStrictAboutOutputDuringTests = false;/*** @var bool*/private $beStrictAboutTodoAnnotatedTests = false;/*** @var bool*/private $beStrictAboutResourceUsageDuringSmallTests = false;/*** @var bool*/private $enforceTimeLimit = false;/*** @var bool*/private $forceCoversAnnotation = false;/*** @var int*/private $timeoutForSmallTests = 1;/*** @var int*/private $timeoutForMediumTests = 10;/*** @var int*/private $timeoutForLargeTests = 60;/*** @var bool*/private $stopOnRisky = false;/*** @var bool*/private $stopOnIncomplete = false;/*** @var bool*/private $stopOnSkipped = false;/*** @var bool*/private $lastTestFailed = false;/*** @var int*/private $defaultTimeLimit = 0;/*** @var bool*/private $stopOnDefect = false;/*** @var bool*/private $registerMockObjectsFromTestArgumentsRecursively = false;/*** @deprecated Use the `TestHook` interfaces instead** @codeCoverageIgnore** Registers a TestListener.*/public function addListener(TestListener $listener): void{$this->listeners[] = $listener;}/*** @deprecated Use the `TestHook` interfaces instead** @codeCoverageIgnore** Unregisters a TestListener.*/public function removeListener(TestListener $listener): void{foreach ($this->listeners as $key => $_listener) {if ($listener === $_listener) {unset($this->listeners[$key]);}}}/*** @deprecated Use the `TestHook` interfaces instead** @codeCoverageIgnore** Flushes all flushable TestListeners.*/public function flushListeners(): void{foreach ($this->listeners as $listener) {if ($listener instanceof Printer) {$listener->flush();}}}/*** Adds an error to the list of errors.*/public function addError(Test $test, Throwable $t, float $time): void{if ($t instanceof RiskyTestError) {$this->recordRisky($test, $t);$notifyMethod = 'addRiskyTest';if ($test instanceof TestCase) {$test->markAsRisky();}if ($this->stopOnRisky || $this->stopOnDefect) {$this->stop();}} elseif ($t instanceof IncompleteTest) {$this->recordNotImplemented($test, $t);$notifyMethod = 'addIncompleteTest';if ($this->stopOnIncomplete) {$this->stop();}} elseif ($t instanceof SkippedTest) {$this->recordSkipped($test, $t);$notifyMethod = 'addSkippedTest';if ($this->stopOnSkipped) {$this->stop();}} else {$this->recordError($test, $t);$notifyMethod = 'addError';if ($this->stopOnError || $this->stopOnFailure) {$this->stop();}}// @see https://github.com/sebastianbergmann/phpunit/issues/1953if ($t instanceof Error) {$t = new ExceptionWrapper($t);}foreach ($this->listeners as $listener) {$listener->{$notifyMethod}($test, $t, $time);}$this->lastTestFailed = true;$this->time += $time;}/*** Adds a warning to the list of warnings.* The passed in exception caused the warning.*/public function addWarning(Test $test, Warning $e, float $time): void{if ($this->stopOnWarning || $this->stopOnDefect) {$this->stop();}$this->recordWarning($test, $e);foreach ($this->listeners as $listener) {$listener->addWarning($test, $e, $time);}$this->time += $time;}/*** Adds a failure to the list of failures.* The passed in exception caused the failure.*/public function addFailure(Test $test, AssertionFailedError $e, float $time): void{if ($e instanceof RiskyTestError || $e instanceof OutputError) {$this->recordRisky($test, $e);$notifyMethod = 'addRiskyTest';if ($test instanceof TestCase) {$test->markAsRisky();}if ($this->stopOnRisky || $this->stopOnDefect) {$this->stop();}} elseif ($e instanceof IncompleteTest) {$this->recordNotImplemented($test, $e);$notifyMethod = 'addIncompleteTest';if ($this->stopOnIncomplete) {$this->stop();}} elseif ($e instanceof SkippedTest) {$this->recordSkipped($test, $e);$notifyMethod = 'addSkippedTest';if ($this->stopOnSkipped) {$this->stop();}} else {$this->failures[] = new TestFailure($test, $e);$notifyMethod = 'addFailure';if ($this->stopOnFailure || $this->stopOnDefect) {$this->stop();}}foreach ($this->listeners as $listener) {$listener->{$notifyMethod}($test, $e, $time);}$this->lastTestFailed = true;$this->time += $time;}/*** Informs the result that a test suite will be started.*/public function startTestSuite(TestSuite $suite): void{$this->currentTestSuiteFailed = false;foreach ($this->listeners as $listener) {$listener->startTestSuite($suite);}}/*** Informs the result that a test suite was completed.*/public function endTestSuite(TestSuite $suite): void{if (!$this->currentTestSuiteFailed) {$this->passedTestClasses[] = $suite->getName();}foreach ($this->listeners as $listener) {$listener->endTestSuite($suite);}}/*** Informs the result that a test will be started.*/public function startTest(Test $test): void{$this->lastTestFailed = false;$this->runTests += count($test);foreach ($this->listeners as $listener) {$listener->startTest($test);}}/*** Informs the result that a test was completed.** @throws \SebastianBergmann\RecursionContext\InvalidArgumentException*/public function endTest(Test $test, float $time): void{foreach ($this->listeners as $listener) {$listener->endTest($test, $time);}if (!$this->lastTestFailed && $test instanceof TestCase) {$class = get_class($test);$key = $class . '::' . $test->getName();$this->passed[$key] = ['result' => $test->getResult(),'size' => TestUtil::getSize($class,$test->getName(false)),];$this->time += $time;}if ($this->lastTestFailed && $test instanceof TestCase) {$this->currentTestSuiteFailed = true;}}/*** Returns true if no risky test occurred.*/public function allHarmless(): bool{return $this->riskyCount() === 0;}/*** Gets the number of risky tests.*/public function riskyCount(): int{return count($this->risky);}/*** Returns true if no incomplete test occurred.*/public function allCompletelyImplemented(): bool{return $this->notImplementedCount() === 0;}/*** Gets the number of incomplete tests.*/public function notImplementedCount(): int{return count($this->notImplemented);}/*** Returns an array of TestFailure objects for the risky tests.** @return TestFailure[]*/public function risky(): array{return $this->risky;}/*** Returns an array of TestFailure objects for the incomplete tests.** @return TestFailure[]*/public function notImplemented(): array{return $this->notImplemented;}/*** Returns true if no test has been skipped.*/public function noneSkipped(): bool{return $this->skippedCount() === 0;}/*** Gets the number of skipped tests.*/public function skippedCount(): int{return count($this->skipped);}/*** Returns an array of TestFailure objects for the skipped tests.** @return TestFailure[]*/public function skipped(): array{return $this->skipped;}/*** Gets the number of detected errors.*/public function errorCount(): int{return count($this->errors);}/*** Returns an array of TestFailure objects for the errors.** @return TestFailure[]*/public function errors(): array{return $this->errors;}/*** Gets the number of detected failures.*/public function failureCount(): int{return count($this->failures);}/*** Returns an array of TestFailure objects for the failures.** @return TestFailure[]*/public function failures(): array{return $this->failures;}/*** Gets the number of detected warnings.*/public function warningCount(): int{return count($this->warnings);}/*** Returns an array of TestFailure objects for the warnings.** @return TestFailure[]*/public function warnings(): array{return $this->warnings;}/*** Returns the names of the tests that have passed.*/public function passed(): array{return $this->passed;}/*** Returns the names of the TestSuites that have passed.** This enables @depends-annotations for TestClassName::class*/public function passedClasses(): array{return $this->passedTestClasses;}/*** Returns whether code coverage information should be collected.*/public function getCollectCodeCoverageInformation(): bool{return $this->codeCoverage !== null;}/*** Runs a TestCase.** @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException* @throws CodeCoverageException* @throws UnintentionallyCoveredCodeException*/public function run(Test $test): void{Assert::resetCount();$size = TestUtil::UNKNOWN;if ($test instanceof TestCase) {$test->setRegisterMockObjectsFromTestArgumentsRecursively($this->registerMockObjectsFromTestArgumentsRecursively);$isAnyCoverageRequired = TestUtil::requiresCodeCoverageDataCollection($test);$size = $test->getSize();}$error = false;$failure = false;$warning = false;$incomplete = false;$risky = false;$skipped = false;$this->startTest($test);if ($this->convertDeprecationsToExceptions || $this->convertErrorsToExceptions || $this->convertNoticesToExceptions || $this->convertWarningsToExceptions) {$errorHandler = new ErrorHandler($this->convertDeprecationsToExceptions,$this->convertErrorsToExceptions,$this->convertNoticesToExceptions,$this->convertWarningsToExceptions);$errorHandler->register();}$collectCodeCoverage = $this->codeCoverage !== null &&!$test instanceof ErrorTestCase &&!$test instanceof WarningTestCase &&$isAnyCoverageRequired;if ($collectCodeCoverage) {$this->codeCoverage->start($test);}$monitorFunctions = $this->beStrictAboutResourceUsageDuringSmallTests &&!$test instanceof ErrorTestCase &&!$test instanceof WarningTestCase &&$size === TestUtil::SMALL &&function_exists('xdebug_start_function_monitor');if ($monitorFunctions) {/* @noinspection ForgottenDebugOutputInspection */xdebug_start_function_monitor(ResourceOperations::getFunctions());}$timer = new Timer;$timer->start();try {$invoker = new Invoker;if (!$test instanceof ErrorTestCase &&!$test instanceof WarningTestCase &&$this->shouldTimeLimitBeEnforced($size) &&$invoker->canInvokeWithTimeout()) {switch ($size) {case TestUtil::SMALL:$_timeout = $this->timeoutForSmallTests;break;case TestUtil::MEDIUM:$_timeout = $this->timeoutForMediumTests;break;case TestUtil::LARGE:$_timeout = $this->timeoutForLargeTests;break;default:$_timeout = $this->defaultTimeLimit;}$invoker->invoke([$test, 'runBare'], [], $_timeout);} else {$test->runBare();}} catch (TimeoutException $e) {$this->addFailure($test,new RiskyTestError($e->getMessage()),$_timeout);$risky = true;} catch (AssertionFailedError $e) {$failure = true;if ($e instanceof RiskyTestError) {$risky = true;} elseif ($e instanceof IncompleteTestError) {$incomplete = true;} elseif ($e instanceof SkippedTestError) {$skipped = true;}} catch (AssertionError $e) {$test->addToAssertionCount(1);$failure = true;$frame = $e->getTrace()[0];$e = new AssertionFailedError(sprintf('%s in %s:%s',$e->getMessage(),$frame['file'] ?? $e->getFile(),$frame['line'] ?? $e->getLine()),0,$e);} catch (Warning $e) {$warning = true;} catch (Exception $e) {$error = true;} catch (Throwable $e) {$e = new ExceptionWrapper($e);$error = true;}$time = $timer->stop()->asSeconds();$test->addToAssertionCount(Assert::getCount());if ($monitorFunctions) {$excludeList = new ExcludeList;/** @noinspection ForgottenDebugOutputInspection */$functions = xdebug_get_monitored_functions();/* @noinspection ForgottenDebugOutputInspection */xdebug_stop_function_monitor();foreach ($functions as $function) {if (!$excludeList->isExcluded($function['filename'])) {$this->addFailure($test,new RiskyTestError(sprintf('%s() used in %s:%s',$function['function'],$function['filename'],$function['lineno'])),$time);}}}if ($this->beStrictAboutTestsThatDoNotTestAnything &&!$test->doesNotPerformAssertions() &&$test->getNumAssertions() === 0) {$risky = true;}if ($this->forceCoversAnnotation && !$error && !$failure && !$warning && !$incomplete && !$skipped && !$risky) {$annotations = TestUtil::parseTestMethodAnnotations(get_class($test),$test->getName(false));if (!isset($annotations['class']['covers']) &&!isset($annotations['method']['covers']) &&!isset($annotations['class']['coversNothing']) &&!isset($annotations['method']['coversNothing'])) {$this->addFailure($test,new MissingCoversAnnotationException('This test does not have a @covers annotation but is expected to have one'),$time);$risky = true;}}if ($collectCodeCoverage) {$append = !$risky && !$incomplete && !$skipped;$linesToBeCovered = [];$linesToBeUsed = [];if ($append && $test instanceof TestCase) {try {$linesToBeCovered = TestUtil::getLinesToBeCovered(get_class($test),$test->getName(false));$linesToBeUsed = TestUtil::getLinesToBeUsed(get_class($test),$test->getName(false));} catch (InvalidCoversTargetException $cce) {$this->addWarning($test,new Warning($cce->getMessage()),$time);}}try {$this->codeCoverage->stop($append,$linesToBeCovered,$linesToBeUsed);} catch (UnintentionallyCoveredCodeException $cce) {$unintentionallyCoveredCodeError = new UnintentionallyCoveredCodeError('This test executed code that is not listed as code to be covered or used:' .PHP_EOL . $cce->getMessage());} catch (OriginalCodeCoverageException $cce) {$error = true;$e = $e ?? $cce;}}if (isset($errorHandler)) {$errorHandler->unregister();unset($errorHandler);}if ($error) {$this->addError($test, $e, $time);} elseif ($failure) {$this->addFailure($test, $e, $time);} elseif ($warning) {$this->addWarning($test, $e, $time);} elseif (isset($unintentionallyCoveredCodeError)) {$this->addFailure($test,$unintentionallyCoveredCodeError,$time);} elseif ($this->beStrictAboutTestsThatDoNotTestAnything &&!$test->doesNotPerformAssertions() &&$test->getNumAssertions() === 0) {try {$reflected = new ReflectionClass($test);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEnd$name = $test->getName(false);if ($name && $reflected->hasMethod($name)) {try {$reflected = $reflected->getMethod($name);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEnd}$this->addFailure($test,new RiskyTestError(sprintf("This test did not perform any assertions\n\n%s:%d",$reflected->getFileName(),$reflected->getStartLine())),$time);} elseif ($this->beStrictAboutTestsThatDoNotTestAnything &&$test->doesNotPerformAssertions() &&$test->getNumAssertions() > 0) {$this->addFailure($test,new RiskyTestError(sprintf('This test is annotated with "@doesNotPerformAssertions" but performed %d assertions',$test->getNumAssertions())),$time);} elseif ($this->beStrictAboutOutputDuringTests && $test->hasOutput()) {$this->addFailure($test,new OutputError(sprintf('This test printed output: %s',$test->getActualOutput())),$time);} elseif ($this->beStrictAboutTodoAnnotatedTests && $test instanceof TestCase) {$annotations = TestUtil::parseTestMethodAnnotations(get_class($test),$test->getName(false));if (isset($annotations['method']['todo'])) {$this->addFailure($test,new RiskyTestError('Test method is annotated with @todo'),$time);}}$this->endTest($test, $time);}/*** Gets the number of run tests.*/public function count(): int{return $this->runTests;}/*** Checks whether the test run should stop.*/public function shouldStop(): bool{return $this->stop;}/*** Marks that the test run should stop.*/public function stop(): void{$this->stop = true;}/*** Returns the code coverage object.*/public function getCodeCoverage(): ?CodeCoverage{return $this->codeCoverage;}/*** Sets the code coverage object.*/public function setCodeCoverage(CodeCoverage $codeCoverage): void{$this->codeCoverage = $codeCoverage;}/*** Enables or disables the deprecation-to-exception conversion.*/public function convertDeprecationsToExceptions(bool $flag): void{$this->convertDeprecationsToExceptions = $flag;}/*** Returns the deprecation-to-exception conversion setting.*/public function getConvertDeprecationsToExceptions(): bool{return $this->convertDeprecationsToExceptions;}/*** Enables or disables the error-to-exception conversion.*/public function convertErrorsToExceptions(bool $flag): void{$this->convertErrorsToExceptions = $flag;}/*** Returns the error-to-exception conversion setting.*/public function getConvertErrorsToExceptions(): bool{return $this->convertErrorsToExceptions;}/*** Enables or disables the notice-to-exception conversion.*/public function convertNoticesToExceptions(bool $flag): void{$this->convertNoticesToExceptions = $flag;}/*** Returns the notice-to-exception conversion setting.*/public function getConvertNoticesToExceptions(): bool{return $this->convertNoticesToExceptions;}/*** Enables or disables the warning-to-exception conversion.*/public function convertWarningsToExceptions(bool $flag): void{$this->convertWarningsToExceptions = $flag;}/*** Returns the warning-to-exception conversion setting.*/public function getConvertWarningsToExceptions(): bool{return $this->convertWarningsToExceptions;}/*** Enables or disables the stopping when an error occurs.*/public function stopOnError(bool $flag): void{$this->stopOnError = $flag;}/*** Enables or disables the stopping when a failure occurs.*/public function stopOnFailure(bool $flag): void{$this->stopOnFailure = $flag;}/*** Enables or disables the stopping when a warning occurs.*/public function stopOnWarning(bool $flag): void{$this->stopOnWarning = $flag;}public function beStrictAboutTestsThatDoNotTestAnything(bool $flag): void{$this->beStrictAboutTestsThatDoNotTestAnything = $flag;}public function isStrictAboutTestsThatDoNotTestAnything(): bool{return $this->beStrictAboutTestsThatDoNotTestAnything;}public function beStrictAboutOutputDuringTests(bool $flag): void{$this->beStrictAboutOutputDuringTests = $flag;}public function isStrictAboutOutputDuringTests(): bool{return $this->beStrictAboutOutputDuringTests;}public function beStrictAboutResourceUsageDuringSmallTests(bool $flag): void{$this->beStrictAboutResourceUsageDuringSmallTests = $flag;}public function isStrictAboutResourceUsageDuringSmallTests(): bool{return $this->beStrictAboutResourceUsageDuringSmallTests;}public function enforceTimeLimit(bool $flag): void{$this->enforceTimeLimit = $flag;}public function enforcesTimeLimit(): bool{return $this->enforceTimeLimit;}public function beStrictAboutTodoAnnotatedTests(bool $flag): void{$this->beStrictAboutTodoAnnotatedTests = $flag;}public function isStrictAboutTodoAnnotatedTests(): bool{return $this->beStrictAboutTodoAnnotatedTests;}public function forceCoversAnnotation(): void{$this->forceCoversAnnotation = true;}public function forcesCoversAnnotation(): bool{return $this->forceCoversAnnotation;}/*** Enables or disables the stopping for risky tests.*/public function stopOnRisky(bool $flag): void{$this->stopOnRisky = $flag;}/*** Enables or disables the stopping for incomplete tests.*/public function stopOnIncomplete(bool $flag): void{$this->stopOnIncomplete = $flag;}/*** Enables or disables the stopping for skipped tests.*/public function stopOnSkipped(bool $flag): void{$this->stopOnSkipped = $flag;}/*** Enables or disables the stopping for defects: error, failure, warning.*/public function stopOnDefect(bool $flag): void{$this->stopOnDefect = $flag;}/*** Returns the time spent running the tests.*/public function time(): float{return $this->time;}/*** Returns whether the entire test was successful or not.*/public function wasSuccessful(): bool{return $this->wasSuccessfulIgnoringWarnings() && empty($this->warnings);}public function wasSuccessfulIgnoringWarnings(): bool{return empty($this->errors) && empty($this->failures);}public function wasSuccessfulAndNoTestIsRiskyOrSkippedOrIncomplete(): bool{return $this->wasSuccessful() && $this->allHarmless() && $this->allCompletelyImplemented() && $this->noneSkipped();}/*** Sets the default timeout for tests.*/public function setDefaultTimeLimit(int $timeout): void{$this->defaultTimeLimit = $timeout;}/*** Sets the timeout for small tests.*/public function setTimeoutForSmallTests(int $timeout): void{$this->timeoutForSmallTests = $timeout;}/*** Sets the timeout for medium tests.*/public function setTimeoutForMediumTests(int $timeout): void{$this->timeoutForMediumTests = $timeout;}/*** Sets the timeout for large tests.*/public function setTimeoutForLargeTests(int $timeout): void{$this->timeoutForLargeTests = $timeout;}/*** Returns the set timeout for large tests.*/public function getTimeoutForLargeTests(): int{return $this->timeoutForLargeTests;}public function setRegisterMockObjectsFromTestArgumentsRecursively(bool $flag): void{$this->registerMockObjectsFromTestArgumentsRecursively = $flag;}private function recordError(Test $test, Throwable $t): void{$this->errors[] = new TestFailure($test, $t);}private function recordNotImplemented(Test $test, Throwable $t): void{$this->notImplemented[] = new TestFailure($test, $t);}private function recordRisky(Test $test, Throwable $t): void{$this->risky[] = new TestFailure($test, $t);}private function recordSkipped(Test $test, Throwable $t): void{$this->skipped[] = new TestFailure($test, $t);}private function recordWarning(Test $test, Throwable $t): void{$this->warnings[] = new TestFailure($test, $t);}private function shouldTimeLimitBeEnforced(int $size): bool{if (!$this->enforceTimeLimit) {return false;}if (!(($this->defaultTimeLimit || $size !== TestUtil::UNKNOWN))) {return false;}if (!extension_loaded('pcntl')) {return false;}if (!class_exists(Invoker::class)) {return false;}if (extension_loaded('xdebug') && xdebug_is_debugger_active()) {return false;}return true;}}