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/AfterLastTestHook.php b/src/Runner/Hook/AfterLastTestHook.php
new file mode 100644
index 00000000000..77034d30b05
--- /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/BeforeFirstTestHook.php b/src/Runner/Hook/BeforeFirstTestHook.php
new file mode 100644
index 00000000000..d7f8e3d4bc6
--- /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/Hook.php b/src/Runner/Hook/Hook.php
new file mode 100644
index 00000000000..680ce7c5db2
--- /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/TextUI/TestRunner.php b/src/TextUI/TestRunner.php
index cb49778a7da..8233ba84e6a 100644
--- a/src/TextUI/TestRunner.php
+++ b/src/TextUI/TestRunner.php
@@ -18,11 +18,14 @@
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\TestSuiteLoader;
use PHPUnit\Runner\Version;
@@ -86,6 +89,11 @@ class TestRunner extends BaseTestRunner
*/
private $messagePrinted = false;
+ /**
+ * @var Hook[]
+ */
+ private $extensions = [];
+
/**
* @param TestSuiteLoader $loader
* @param CodeCoverageFilter $filter
@@ -505,8 +513,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 +912,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..1407739cfb6
--- /dev/null
+++ b/tests/TextUI/_files/Extension.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/tests/TextUI/hooks.phpt b/tests/TextUI/hooks.phpt
new file mode 100644
index 00000000000..ef88193ac59
--- /dev/null
+++ b/tests/TextUI/hooks.phpt
@@ -0,0 +1,16 @@
+--TEST--
+phpunit --configuration _files/hooks.xml BankAccountTest ../_files/BankAccountTest.php
+--FILE--
+