From 41c75847e0a7e7df2e0eba6b6edf4d1b99da3103 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 21 Feb 2018 14:46:15 +0100 Subject: [PATCH] Closes #3002 --- ChangeLog-7.1.md | 4 + phpunit.xsd | 6 + src/Runner/Hook/AfterIncompleteTestHook.php | 16 +++ src/Runner/Hook/AfterLastTestHook.php | 16 +++ src/Runner/Hook/AfterRiskyTestHook.php | 16 +++ src/Runner/Hook/AfterSkippedTestHook.php | 16 +++ src/Runner/Hook/AfterSuccessfulTestHook.php | 16 +++ src/Runner/Hook/AfterTestErrorHook.php | 16 +++ src/Runner/Hook/AfterTestFailureHook.php | 16 +++ src/Runner/Hook/AfterTestWarningHook.php | 16 +++ src/Runner/Hook/BeforeFirstTestHook.php | 16 +++ src/Runner/Hook/BeforeTestHook.php | 16 +++ src/Runner/Hook/Hook.php | 15 +++ src/Runner/Hook/TestHook.php | 15 +++ src/Runner/Hook/TestListenerAdapter.php | 134 ++++++++++++++++++++ src/TextUI/TestRunner.php | 67 ++++++++++ src/Util/Configuration.php | 25 ++++ tests/TextUI/_files/Extension.php | 66 ++++++++++ tests/TextUI/_files/HookTest.php | 44 +++++++ tests/TextUI/_files/NullPrinter.php | 11 ++ tests/TextUI/_files/hooks.xml | 8 ++ tests/TextUI/hooks.phpt | 30 +++++ 22 files changed, 585 insertions(+) create mode 100644 src/Runner/Hook/AfterIncompleteTestHook.php create mode 100644 src/Runner/Hook/AfterLastTestHook.php create mode 100644 src/Runner/Hook/AfterRiskyTestHook.php create mode 100644 src/Runner/Hook/AfterSkippedTestHook.php create mode 100644 src/Runner/Hook/AfterSuccessfulTestHook.php create mode 100644 src/Runner/Hook/AfterTestErrorHook.php create mode 100644 src/Runner/Hook/AfterTestFailureHook.php create mode 100644 src/Runner/Hook/AfterTestWarningHook.php create mode 100644 src/Runner/Hook/BeforeFirstTestHook.php create mode 100644 src/Runner/Hook/BeforeTestHook.php create mode 100644 src/Runner/Hook/Hook.php create mode 100644 src/Runner/Hook/TestHook.php create mode 100644 src/Runner/Hook/TestListenerAdapter.php create mode 100644 tests/TextUI/_files/Extension.php create mode 100644 tests/TextUI/_files/HookTest.php create mode 100644 tests/TextUI/_files/NullPrinter.php create mode 100644 tests/TextUI/_files/hooks.xml create mode 100644 tests/TextUI/hooks.phpt diff --git a/ChangeLog-7.1.md b/ChangeLog-7.1.md index 2682cae76bb..07853de2361 100644 --- a/ChangeLog-7.1.md +++ b/ChangeLog-7.1.md @@ -4,6 +4,10 @@ All notable changes of the PHPUnit 7.1 release series are documented in this fil ## [7.1.0] - 2018-04-06 +### Added + +* Implemented [#3002](https://github.com/sebastianbergmann/phpunit/issues/3002): Support for test runner extensions + ### Changed * `PHPUnit\Framework\Assert` is no longer searched for test methods diff --git a/phpunit.xsd b/phpunit.xsd index 3d54573c50d..81690e13cd1 100644 --- a/phpunit.xsd +++ b/phpunit.xsd @@ -50,6 +50,11 @@ + + + + + @@ -248,6 +253,7 @@ + diff --git a/src/Runner/Hook/AfterIncompleteTestHook.php b/src/Runner/Hook/AfterIncompleteTestHook.php new file mode 100644 index 00000000000..1b352d30c66 --- /dev/null +++ b/src/Runner/Hook/AfterIncompleteTestHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterIncompleteTestHook extends TestHook +{ + public function executeAfterIncompleteTest(string $test, string $message, float $time): void; +} diff --git a/src/Runner/Hook/AfterLastTestHook.php b/src/Runner/Hook/AfterLastTestHook.php new file mode 100644 index 00000000000..b5d2e2efd6c --- /dev/null +++ b/src/Runner/Hook/AfterLastTestHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterLastTestHook extends Hook +{ + public function executeAfterLastTest(): void; +} diff --git a/src/Runner/Hook/AfterRiskyTestHook.php b/src/Runner/Hook/AfterRiskyTestHook.php new file mode 100644 index 00000000000..0a7cb980a85 --- /dev/null +++ b/src/Runner/Hook/AfterRiskyTestHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterRiskyTestHook extends TestHook +{ + public function executeAfterRiskyTest(string $test, string $message, float $time): void; +} diff --git a/src/Runner/Hook/AfterSkippedTestHook.php b/src/Runner/Hook/AfterSkippedTestHook.php new file mode 100644 index 00000000000..460c81d9ee9 --- /dev/null +++ b/src/Runner/Hook/AfterSkippedTestHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterSkippedTestHook extends TestHook +{ + public function executeAfterSkippedTest(string $test, string $message, float $time): void; +} diff --git a/src/Runner/Hook/AfterSuccessfulTestHook.php b/src/Runner/Hook/AfterSuccessfulTestHook.php new file mode 100644 index 00000000000..175f463dbc5 --- /dev/null +++ b/src/Runner/Hook/AfterSuccessfulTestHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterSuccessfulTestHook extends TestHook +{ + public function executeAfterSuccessfulTest(string $test, float $time): void; +} diff --git a/src/Runner/Hook/AfterTestErrorHook.php b/src/Runner/Hook/AfterTestErrorHook.php new file mode 100644 index 00000000000..ff2ba0ed84b --- /dev/null +++ b/src/Runner/Hook/AfterTestErrorHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterTestErrorHook extends TestHook +{ + public function executeAfterTestError(string $test, string $message, float $time): void; +} diff --git a/src/Runner/Hook/AfterTestFailureHook.php b/src/Runner/Hook/AfterTestFailureHook.php new file mode 100644 index 00000000000..351667e5275 --- /dev/null +++ b/src/Runner/Hook/AfterTestFailureHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterTestFailureHook extends TestHook +{ + public function executeAfterTestFailure(string $test, string $message, float $time): void; +} diff --git a/src/Runner/Hook/AfterTestWarningHook.php b/src/Runner/Hook/AfterTestWarningHook.php new file mode 100644 index 00000000000..19ec90875a5 --- /dev/null +++ b/src/Runner/Hook/AfterTestWarningHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface AfterTestWarningHook extends TestHook +{ + public function executeAfterTestWarning(string $test, string $message, float $time): void; +} diff --git a/src/Runner/Hook/BeforeFirstTestHook.php b/src/Runner/Hook/BeforeFirstTestHook.php new file mode 100644 index 00000000000..99c4dbd2bde --- /dev/null +++ b/src/Runner/Hook/BeforeFirstTestHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface BeforeFirstTestHook extends Hook +{ + public function executeBeforeFirstTest(): void; +} diff --git a/src/Runner/Hook/BeforeTestHook.php b/src/Runner/Hook/BeforeTestHook.php new file mode 100644 index 00000000000..a0383445bb7 --- /dev/null +++ b/src/Runner/Hook/BeforeTestHook.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface BeforeTestHook extends TestHook +{ + public function executeBeforeTest(string $test): void; +} diff --git a/src/Runner/Hook/Hook.php b/src/Runner/Hook/Hook.php new file mode 100644 index 00000000000..2eeaf5b59fe --- /dev/null +++ b/src/Runner/Hook/Hook.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface Hook +{ +} diff --git a/src/Runner/Hook/TestHook.php b/src/Runner/Hook/TestHook.php new file mode 100644 index 00000000000..02d7fc6c08a --- /dev/null +++ b/src/Runner/Hook/TestHook.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +interface TestHook extends Hook +{ +} diff --git a/src/Runner/Hook/TestListenerAdapter.php b/src/Runner/Hook/TestListenerAdapter.php new file mode 100644 index 00000000000..5be8298fd27 --- /dev/null +++ b/src/Runner/Hook/TestListenerAdapter.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPUnit\Runner; + +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Test; +use PHPUnit\Framework\TestListener; +use PHPUnit\Framework\TestSuite; +use PHPUnit\Framework\Warning; +use PHPUnit\Util\Test as TestUtil; + +final class TestListenerAdapter implements TestListener +{ + /** + * @var TestHook[] + */ + private $hooks = []; + + /** + * @var bool + */ + private $lastTestWasNotSuccessful; + + public function add(TestHook $hook): void + { + $this->hooks[] = $hook; + } + + public function startTest(Test $test): void + { + foreach ($this->hooks as $hook) { + if ($hook instanceof BeforeTestHook) { + $hook->executeBeforeTest(TestUtil::describeAsString($test)); + } + } + + $this->lastTestWasNotSuccessful = false; + } + + public function addError(Test $test, \Throwable $t, float $time): void + { + foreach ($this->hooks as $hook) { + if ($hook instanceof AfterTestErrorHook) { + $hook->executeAfterTestError(TestUtil::describeAsString($test), $t->getMessage(), $time); + } + } + + $this->lastTestWasNotSuccessful = true; + } + + public function addWarning(Test $test, Warning $e, float $time): void + { + foreach ($this->hooks as $hook) { + if ($hook instanceof AfterTestWarningHook) { + $hook->executeAfterTestWarning(TestUtil::describeAsString($test), $e->getMessage(), $time); + } + } + + $this->lastTestWasNotSuccessful = true; + } + + public function addFailure(Test $test, AssertionFailedError $e, float $time): void + { + foreach ($this->hooks as $hook) { + if ($hook instanceof AfterTestFailureHook) { + $hook->executeAfterTestFailure(TestUtil::describeAsString($test), $e->getMessage(), $time); + } + } + + $this->lastTestWasNotSuccessful = true; + } + + public function addIncompleteTest(Test $test, \Throwable $t, float $time): void + { + foreach ($this->hooks as $hook) { + if ($hook instanceof AfterIncompleteTestHook) { + $hook->executeAfterIncompleteTest(TestUtil::describeAsString($test), $t->getMessage(), $time); + } + } + + $this->lastTestWasNotSuccessful = true; + } + + public function addRiskyTest(Test $test, \Throwable $t, float $time): void + { + foreach ($this->hooks as $hook) { + if ($hook instanceof AfterRiskyTestHook) { + $hook->executeAfterRiskyTest(TestUtil::describeAsString($test), $t->getMessage(), $time); + } + } + + $this->lastTestWasNotSuccessful = true; + } + + public function addSkippedTest(Test $test, \Throwable $t, float $time): void + { + foreach ($this->hooks as $hook) { + if ($hook instanceof AfterSkippedTestHook) { + $hook->executeAfterSkippedTest(TestUtil::describeAsString($test), $t->getMessage(), $time); + } + } + + $this->lastTestWasNotSuccessful = true; + } + + public function endTest(Test $test, float $time): void + { + if ($this->lastTestWasNotSuccessful === true) { + return; + } + + foreach ($this->hooks as $hook) { + if ($hook instanceof AfterSuccessfulTestHook) { + $hook->executeAfterSuccessfulTest(TestUtil::describeAsString($test), $time); + } + } + } + + public function startTestSuite(TestSuite $suite): void + { + } + + public function endTestSuite(TestSuite $suite): void + { + } +} diff --git a/src/TextUI/TestRunner.php b/src/TextUI/TestRunner.php index cb49778a7da..6d2fe31dd7e 100644 --- a/src/TextUI/TestRunner.php +++ b/src/TextUI/TestRunner.php @@ -18,12 +18,17 @@ use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestResult; use PHPUnit\Framework\TestSuite; +use PHPUnit\Runner\AfterLastTestHook; use PHPUnit\Runner\BaseTestRunner; +use PHPUnit\Runner\BeforeFirstTestHook; use PHPUnit\Runner\Filter\ExcludeGroupFilterIterator; use PHPUnit\Runner\Filter\Factory; use PHPUnit\Runner\Filter\IncludeGroupFilterIterator; use PHPUnit\Runner\Filter\NameFilterIterator; +use PHPUnit\Runner\Hook; use PHPUnit\Runner\StandardTestSuiteLoader; +use PHPUnit\Runner\TestHook; +use PHPUnit\Runner\TestListenerAdapter; use PHPUnit\Runner\TestSuiteLoader; use PHPUnit\Runner\Version; use PHPUnit\Util\Configuration; @@ -86,6 +91,11 @@ class TestRunner extends BaseTestRunner */ private $messagePrinted = false; + /** + * @var Hook[] + */ + private $extensions = []; + /** * @param TestSuiteLoader $loader * @param CodeCoverageFilter $filter @@ -183,6 +193,23 @@ public function doRun(Test $suite, array $arguments = [], $exit = true): TestRes $result = $this->createTestResult(); + $listener = new TestListenerAdapter; + $listenerNeeded = false; + + foreach ($this->extensions as $extension) { + if ($extension instanceof TestHook) { + $listener->add($extension); + + $listenerNeeded = true; + } + } + + if ($listenerNeeded) { + $result->addListener($listener); + } + + unset($listener, $listenerNeeded); + if (!$arguments['convertErrorsToExceptions']) { $result->convertErrorsToExceptions(false); } @@ -505,8 +532,20 @@ public function doRun(Test $suite, array $arguments = [], $exit = true): TestRes $suite->setRunTestInSeparateProcess($arguments['processIsolation']); } + foreach ($this->extensions as $extension) { + if ($extension instanceof BeforeFirstTestHook) { + $extension->executeBeforeFirstTest(); + } + } + $suite->run($result); + foreach ($this->extensions as $extension) { + if ($extension instanceof AfterLastTestHook) { + $extension->executeAfterLastTest(); + } + } + $result->flushListeners(); if ($this->printer instanceof ResultPrinter) { @@ -892,6 +931,34 @@ protected function handleConfiguration(array &$arguments): void $arguments['excludeGroups'] = \array_diff($groupConfiguration['exclude'], $groupCliArgs); } + foreach ($arguments['configuration']->getExtensionConfiguration() as $extension) { + if (!\class_exists($extension['class'], false) && $extension['file'] !== '') { + require_once $extension['file']; + } + + if (!\class_exists($extension['class'])) { + throw new Exception( + \sprintf( + 'Class "%s" does not exist', + $extension['class'] + ) + ); + } + + $extensionClass = new ReflectionClass($extension['class']); + + if (!$extensionClass->implementsInterface(Hook::class)) { + throw new Exception( + \sprintf( + 'Class "%s" does not implement a PHPUnit\Runner\Hook interface', + $extension['class'] + ) + ); + } + + $this->extensions[] = $extensionClass->newInstance(); + } + foreach ($arguments['configuration']->getListenerConfiguration() as $listener) { if (!\class_exists($listener['class'], false) && $listener['file'] !== '') { diff --git a/src/Util/Configuration.php b/src/Util/Configuration.php index c1dcdd6433d..b4b741997de 100644 --- a/src/Util/Configuration.php +++ b/src/Util/Configuration.php @@ -231,6 +231,31 @@ public function getFilename(): string return $this->filename; } + public function getExtensionConfiguration(): array + { + $result = []; + + foreach ($this->xpath->query('extensions/extension') as $extension) { + /** @var DOMElement $extension */ + $class = (string) $extension->getAttribute('class'); + $file = ''; + + if ($extension->getAttribute('file')) { + $file = $this->toAbsolutePath( + (string) $extension->getAttribute('file'), + true + ); + } + + $result[] = [ + 'class' => $class, + 'file' => $file + ]; + } + + return $result; + } + /** * Returns the configuration for SUT filtering. * diff --git a/tests/TextUI/_files/Extension.php b/tests/TextUI/_files/Extension.php new file mode 100644 index 00000000000..e7b0066ae83 --- /dev/null +++ b/tests/TextUI/_files/Extension.php @@ -0,0 +1,66 @@ +assertTrue(true); + } + + public function testFailure(): void + { + $this->assertTrue(false); + } + + public function testError(): void + { + throw new \Exception('message'); + } + + public function testIncomplete(): void + { + $this->markTestIncomplete('message'); + } + + public function testRisky(): void + { + throw new RiskyTestError('message'); + } + + public function testSkipped(): void + { + $this->markTestSkipped('message'); + } + + public function testWarning(): void + { + throw new Warning('message'); + } +} diff --git a/tests/TextUI/_files/NullPrinter.php b/tests/TextUI/_files/NullPrinter.php new file mode 100644 index 00000000000..3d298ff83fc --- /dev/null +++ b/tests/TextUI/_files/NullPrinter.php @@ -0,0 +1,11 @@ + + + + + + diff --git a/tests/TextUI/hooks.phpt b/tests/TextUI/hooks.phpt new file mode 100644 index 00000000000..778416426f8 --- /dev/null +++ b/tests/TextUI/hooks.phpt @@ -0,0 +1,30 @@ +--TEST-- +phpunit --configuration _files/hooks.xml HookTest _files/HookTest.php +--FILE-- +