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 array_keys;use function array_map;use function array_merge;use function array_slice;use function array_unique;use function basename;use function call_user_func;use function class_exists;use function count;use function dirname;use function get_declared_classes;use function implode;use function is_bool;use function is_callable;use function is_file;use function is_object;use function is_string;use function method_exists;use function preg_match;use function preg_quote;use function sprintf;use function strpos;use function substr;use Iterator;use IteratorAggregate;use PHPUnit\Runner\BaseTestRunner;use PHPUnit\Runner\Filter\Factory;use PHPUnit\Runner\PhptTestCase;use PHPUnit\Util\FileLoader;use PHPUnit\Util\Reflection;use PHPUnit\Util\Test as TestUtil;use ReflectionClass;use ReflectionException;use ReflectionMethod;use Throwable;/*** @template-implements IteratorAggregate<int, Test>** @internal This class is not covered by the backward compatibility promise for PHPUnit*/class TestSuite implements IteratorAggregate, Reorderable, SelfDescribing, Test{/*** Enable or disable the backup and restoration of the $GLOBALS array.** @var bool*/protected $backupGlobals;/*** Enable or disable the backup and restoration of static attributes.** @var bool*/protected $backupStaticAttributes;/*** @var bool*/protected $runTestInSeparateProcess = false;/*** The name of the test suite.** @var string*/protected $name = '';/*** The test groups of the test suite.** @psalm-var array<string,list<Test>>*/protected $groups = [];/*** The tests in the test suite.** @var Test[]*/protected $tests = [];/*** The number of tests in the test suite.** @var int*/protected $numTests = -1;/*** @var bool*/protected $testCase = false;/*** @var string[]*/protected $foundClasses = [];/*** @var null|list<ExecutionOrderDependency>*/protected $providedTests;/*** @var null|list<ExecutionOrderDependency>*/protected $requiredTests;/*** @var bool*/private $beStrictAboutChangesToGlobalState;/*** @var Factory*/private $iteratorFilter;/*** @var int*/private $declaredClassesPointer;/*** @psalm-var array<int,string>*/private $warnings = [];/*** Constructs a new TestSuite.** - PHPUnit\Framework\TestSuite() constructs an empty TestSuite.** - PHPUnit\Framework\TestSuite(ReflectionClass) constructs a* TestSuite from the given class.** - PHPUnit\Framework\TestSuite(ReflectionClass, String)* constructs a TestSuite from the given class with the given* name.** - PHPUnit\Framework\TestSuite(String) either constructs a* TestSuite from the given class (if the passed string is the* name of an existing class) or constructs an empty TestSuite* with the given name.** @param ReflectionClass|string $theClass** @throws Exception*/public function __construct($theClass = '', string $name = ''){if (!is_string($theClass) && !$theClass instanceof ReflectionClass) {throw InvalidArgumentException::create(1,'ReflectionClass object or string');}$this->declaredClassesPointer = count(get_declared_classes());if (!$theClass instanceof ReflectionClass) {if (class_exists($theClass, true)) {if ($name === '') {$name = $theClass;}try {$theClass = new ReflectionClass($theClass);} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEnd} else {$this->setName($theClass);return;}}if (!$theClass->isSubclassOf(TestCase::class)) {$this->setName((string) $theClass);return;}if ($name !== '') {$this->setName($name);} else {$this->setName($theClass->getName());}$constructor = $theClass->getConstructor();if ($constructor !== null &&!$constructor->isPublic()) {$this->addTest(new WarningTestCase(sprintf('Class "%s" has no public constructor.',$theClass->getName())));return;}foreach ((new Reflection)->publicMethodsInTestClass($theClass) as $method) {if (!TestUtil::isTestMethod($method)) {continue;}$this->addTestMethod($theClass, $method);}if (empty($this->tests)) {$this->addTest(new WarningTestCase(sprintf('No tests found in class "%s".',$theClass->getName())));}$this->testCase = true;}/*** Returns a string representation of the test suite.*/public function toString(): string{return $this->getName();}/*** Adds a test to the suite.** @param array $groups*/public function addTest(Test $test, $groups = []): void{try {$class = new ReflectionClass($test);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEndif (!$class->isAbstract()) {$this->tests[] = $test;$this->clearCaches();if ($test instanceof self && empty($groups)) {$groups = $test->getGroups();}if ($this->containsOnlyVirtualGroups($groups)) {$groups[] = 'default';}foreach ($groups as $group) {if (!isset($this->groups[$group])) {$this->groups[$group] = [$test];} else {$this->groups[$group][] = $test;}}if ($test instanceof TestCase) {$test->setGroups($groups);}}}/*** Adds the tests from the given class to the suite.** @psalm-param object|class-string $testClass** @throws Exception*/public function addTestSuite($testClass): void{if (!(is_object($testClass) || (is_string($testClass) && class_exists($testClass)))) {throw InvalidArgumentException::create(1,'class name or object');}if (!is_object($testClass)) {try {$testClass = new ReflectionClass($testClass);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEnd}if ($testClass instanceof self) {$this->addTest($testClass);} elseif ($testClass instanceof ReflectionClass) {$suiteMethod = false;if (!$testClass->isAbstract() && $testClass->hasMethod(BaseTestRunner::SUITE_METHODNAME)) {try {$method = $testClass->getMethod(BaseTestRunner::SUITE_METHODNAME);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEndif ($method->isStatic()) {$this->addTest($method->invoke(null, $testClass->getName()));$suiteMethod = true;}}if (!$suiteMethod && !$testClass->isAbstract() && $testClass->isSubclassOf(TestCase::class)) {$this->addTest(new self($testClass));}} else {throw new Exception;}}public function addWarning(string $warning): void{$this->warnings[] = $warning;}/*** Wraps both <code>addTest()</code> and <code>addTestSuite</code>* as well as the separate import statements for the user's convenience.** If the named file cannot be read or there are no new tests that can be* added, a <code>PHPUnit\Framework\WarningTestCase</code> will be created instead,* leaving the current test run untouched.** @throws Exception*/public function addTestFile(string $filename): void{if (is_file($filename) && substr($filename, -5) === '.phpt') {$this->addTest(new PhptTestCase($filename));$this->declaredClassesPointer = count(get_declared_classes());return;}$numTests = count($this->tests);// The given file may contain further stub classes in addition to the// test class itself. Figure out the actual test class.$filename = FileLoader::checkAndLoad($filename);$newClasses = array_slice(get_declared_classes(), $this->declaredClassesPointer);// The diff is empty in case a parent class (with test methods) is added// AFTER a child class that inherited from it. To account for that case,// accumulate all discovered classes, so the parent class may be found in// a later invocation.if (!empty($newClasses)) {// On the assumption that test classes are defined first in files,// process discovered classes in approximate LIFO order, so as to// avoid unnecessary reflection.$this->foundClasses = array_merge($newClasses, $this->foundClasses);$this->declaredClassesPointer = count(get_declared_classes());}// The test class's name must match the filename, either in full, or as// a PEAR/PSR-0 prefixed short name ('NameSpace_ShortName'), or as a// PSR-1 local short name ('NameSpace\ShortName'). The comparison must be// anchored to prevent false-positive matches (e.g., 'OtherShortName').$shortName = basename($filename, '.php');$shortNameRegEx = '/(?:^|_|\\\\)' . preg_quote($shortName, '/') . '$/';foreach ($this->foundClasses as $i => $className) {if (preg_match($shortNameRegEx, $className)) {try {$class = new ReflectionClass($className);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEndif ($class->getFileName() == $filename) {$newClasses = [$className];unset($this->foundClasses[$i]);break;}}}foreach ($newClasses as $className) {try {$class = new ReflectionClass($className);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEndif (dirname($class->getFileName()) === __DIR__) {continue;}if ($class->isAbstract() && $class->isSubclassOf(TestCase::class)) {$this->addWarning(sprintf('Abstract test case classes with "Test" suffix are deprecated (%s)',$class->getName()));}if (!$class->isAbstract()) {if ($class->hasMethod(BaseTestRunner::SUITE_METHODNAME)) {try {$method = $class->getMethod(BaseTestRunner::SUITE_METHODNAME);// @codeCoverageIgnoreStart} catch (ReflectionException $e) {throw new Exception($e->getMessage(),$e->getCode(),$e);}// @codeCoverageIgnoreEndif ($method->isStatic()) {$this->addTest($method->invoke(null, $className));}} elseif ($class->implementsInterface(Test::class)) {// Do we have modern namespacing ('Foo\Bar\WhizBangTest') or old-school namespacing ('Foo_Bar_WhizBangTest')?$isPsr0 = (!$class->inNamespace()) && (strpos($class->getName(), '_') !== false);$expectedClassName = $isPsr0 ? $className : $shortName;if (($pos = strpos($expectedClassName, '.')) !== false) {$expectedClassName = substr($expectedClassName,0,$pos);}if ($class->getShortName() !== $expectedClassName) {$this->addWarning(sprintf("Test case class not matching filename is deprecated\n in %s\n Class name was '%s', expected '%s'",$filename,$class->getShortName(),$expectedClassName));}$this->addTestSuite($class);}}}if (count($this->tests) > ++$numTests) {$this->addWarning(sprintf("Multiple test case classes per file is deprecated\n in %s",$filename));}$this->numTests = -1;}/*** Wrapper for addTestFile() that adds multiple test files.** @throws Exception*/public function addTestFiles(iterable $fileNames): void{foreach ($fileNames as $filename) {$this->addTestFile((string) $filename);}}/*** Counts the number of test cases that will be run by this test.** @todo refactor usage of numTests in DefaultResultPrinter*/public function count(): int{$this->numTests = 0;foreach ($this as $test) {$this->numTests += count($test);}return $this->numTests;}/*** Returns the name of the suite.*/public function getName(): string{return $this->name;}/*** Returns the test groups of the suite.** @psalm-return list<string>*/public function getGroups(): array{return array_map(static function ($key): string{return (string) $key;},array_keys($this->groups));}public function getGroupDetails(): array{return $this->groups;}/*** Set tests groups of the test case.*/public function setGroupDetails(array $groups): void{$this->groups = $groups;}/*** Runs the tests and collects their result in a TestResult.** @throws \PHPUnit\Framework\CodeCoverageException* @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException* @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException* @throws Warning*/public function run(TestResult $result = null): TestResult{if ($result === null) {$result = $this->createResult();}if (count($this) === 0) {return $result;}/** @psalm-var class-string $className */$className = $this->name;$hookMethods = TestUtil::getHookMethods($className);$result->startTestSuite($this);$test = null;if ($this->testCase && class_exists($this->name, false)) {try {foreach ($hookMethods['beforeClass'] as $beforeClassMethod) {if (method_exists($this->name, $beforeClassMethod)) {if ($missingRequirements = TestUtil::getMissingRequirements($this->name, $beforeClassMethod)) {$this->markTestSuiteSkipped(implode(PHP_EOL, $missingRequirements));}call_user_func([$this->name, $beforeClassMethod]);}}} catch (SkippedTestSuiteError|SkippedTestError $error) {foreach ($this->tests() as $test) {$result->startTest($test);$result->addFailure($test, $error, 0);$result->endTest($test, 0);}$result->endTestSuite($this);return $result;} catch (Throwable $t) {$errorAdded = false;foreach ($this->tests() as $test) {if ($result->shouldStop()) {break;}$result->startTest($test);if (!$errorAdded) {$result->addError($test, $t, 0);$errorAdded = true;} else {$result->addFailure($test,new SkippedTestError('Test skipped because of an error in hook method'),0);}$result->endTest($test, 0);}$result->endTestSuite($this);return $result;}}foreach ($this as $test) {if ($result->shouldStop()) {break;}if ($test instanceof TestCase || $test instanceof self) {$test->setBeStrictAboutChangesToGlobalState($this->beStrictAboutChangesToGlobalState);$test->setBackupGlobals($this->backupGlobals);$test->setBackupStaticAttributes($this->backupStaticAttributes);$test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);}$test->run($result);}if ($this->testCase && class_exists($this->name, false)) {foreach ($hookMethods['afterClass'] as $afterClassMethod) {if (method_exists($this->name, $afterClassMethod)) {try {call_user_func([$this->name, $afterClassMethod]);} catch (Throwable $t) {$message = "Exception in {$this->name}::{$afterClassMethod}" . PHP_EOL . $t->getMessage();$error = new SyntheticError($message, 0, $t->getFile(), $t->getLine(), $t->getTrace());$placeholderTest = clone $test;$placeholderTest->setName($afterClassMethod);$result->startTest($placeholderTest);$result->addFailure($placeholderTest, $error, 0);$result->endTest($placeholderTest, 0);}}}}$result->endTestSuite($this);return $result;}public function setRunTestInSeparateProcess(bool $runTestInSeparateProcess): void{$this->runTestInSeparateProcess = $runTestInSeparateProcess;}public function setName(string $name): void{$this->name = $name;}/*** Returns the tests as an enumeration.** @return Test[]*/public function tests(): array{return $this->tests;}/*** Set tests of the test suite.** @param Test[] $tests*/public function setTests(array $tests): void{$this->tests = $tests;}/*** Mark the test suite as skipped.** @param string $message** @throws SkippedTestSuiteError** @psalm-return never-return*/public function markTestSuiteSkipped($message = ''): void{throw new SkippedTestSuiteError($message);}/*** @param bool $beStrictAboutChangesToGlobalState*/public function setBeStrictAboutChangesToGlobalState($beStrictAboutChangesToGlobalState): void{if (null === $this->beStrictAboutChangesToGlobalState && is_bool($beStrictAboutChangesToGlobalState)) {$this->beStrictAboutChangesToGlobalState = $beStrictAboutChangesToGlobalState;}}/*** @param bool $backupGlobals*/public function setBackupGlobals($backupGlobals): void{if (null === $this->backupGlobals && is_bool($backupGlobals)) {$this->backupGlobals = $backupGlobals;}}/*** @param bool $backupStaticAttributes*/public function setBackupStaticAttributes($backupStaticAttributes): void{if (null === $this->backupStaticAttributes && is_bool($backupStaticAttributes)) {$this->backupStaticAttributes = $backupStaticAttributes;}}/*** Returns an iterator for this test suite.*/public function getIterator(): Iterator{$iterator = new TestSuiteIterator($this);if ($this->iteratorFilter !== null) {$iterator = $this->iteratorFilter->factory($iterator, $this);}return $iterator;}public function injectFilter(Factory $filter): void{$this->iteratorFilter = $filter;foreach ($this as $test) {if ($test instanceof self) {$test->injectFilter($filter);}}}/*** @psalm-return array<int,string>*/public function warnings(): array{return array_unique($this->warnings);}/*** @return list<ExecutionOrderDependency>*/public function provides(): array{if ($this->providedTests === null) {$this->providedTests = [];if (is_callable($this->sortId(), true)) {$this->providedTests[] = new ExecutionOrderDependency($this->sortId());}foreach ($this->tests as $test) {if (!($test instanceof Reorderable)) {// @codeCoverageIgnoreStartcontinue;// @codeCoverageIgnoreEnd}$this->providedTests = ExecutionOrderDependency::mergeUnique($this->providedTests, $test->provides());}}return $this->providedTests;}/*** @return list<ExecutionOrderDependency>*/public function requires(): array{if ($this->requiredTests === null) {$this->requiredTests = [];foreach ($this->tests as $test) {if (!($test instanceof Reorderable)) {// @codeCoverageIgnoreStartcontinue;// @codeCoverageIgnoreEnd}$this->requiredTests = ExecutionOrderDependency::mergeUnique(ExecutionOrderDependency::filterInvalid($this->requiredTests),$test->requires());}$this->requiredTests = ExecutionOrderDependency::diff($this->requiredTests, $this->provides());}return $this->requiredTests;}public function sortId(): string{return $this->getName() . '::class';}/*** Creates a default TestResult object.*/protected function createResult(): TestResult{return new TestResult;}/*** @throws Exception*/protected function addTestMethod(ReflectionClass $class, ReflectionMethod $method): void{$methodName = $method->getName();$test = (new TestBuilder)->build($class, $methodName);if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) {$test->setDependencies(TestUtil::getDependencies($class->getName(), $methodName));}$this->addTest($test,TestUtil::getGroups($class->getName(), $methodName));}private function clearCaches(): void{$this->numTests = -1;$this->providedTests = null;$this->requiredTests = null;}private function containsOnlyVirtualGroups(array $groups): bool{foreach ($groups as $group) {if (strpos($group, '__phpunit_') !== 0) {return false;}}return true;}}