-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement filtering tests based on XML input file #4449
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
<?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\Runner\Filter; | ||
|
||
use DOMElement; | ||
use DOMXPath; | ||
use Exception; | ||
use Generator; | ||
use PHPUnit\Framework\TestCase; | ||
use PHPUnit\Framework\TestSuite; | ||
use PHPUnit\Runner\PhptTestCase; | ||
use PHPUnit\Util\Xml\Loader as XmlLoader; | ||
use RecursiveFilterIterator; | ||
use RecursiveIterator; | ||
|
||
/** | ||
* @internal This class is not covered by the backward compatibility promise for PHPUnit | ||
*/ | ||
final class XmlTestsIterator extends RecursiveFilterIterator | ||
{ | ||
/** | ||
* The filter is used as a fast look for | ||
* - the class name | ||
* - the method name plus its optional data set description. | ||
* | ||
* Example: `filter[class name][method name + data set] = true;` | ||
* | ||
* The accept() method then can use a fast isset() to check if a test should | ||
* be included or not. | ||
* | ||
* This works equally for phpt tests, except we hardcode the class name. | ||
* | ||
* @var array<string,array<string,true>> | ||
*/ | ||
private array $filter; | ||
|
||
/** | ||
* @throws \PHPUnit\Util\Xml\Exception | ||
* | ||
* @return array<string,array<string,true>> | ||
*/ | ||
public static function createFilterFromXmlFile(string $xmlFile): array | ||
{ | ||
$xml = (new XmlLoader())->loadFile($xmlFile); | ||
$xpath = new DOMXPath($xml); | ||
$filter = []; | ||
|
||
foreach (self::extractTestCases($xpath) as [$className, $methodName, $dataSet]) { | ||
if (!isset($filter[$className])) { | ||
$filter[$className] = []; | ||
} | ||
|
||
if (!$dataSet) { | ||
$filter[$className][$methodName] = true; | ||
|
||
continue; | ||
} | ||
|
||
$name = "{$methodName} with data set {$dataSet}"; | ||
$filter[$className][$name] = true; | ||
} | ||
|
||
foreach (self::extractPhptFile($xpath) as $path) { | ||
$filter[PhptTestCase::class][$path] = true; | ||
} | ||
|
||
return $filter; | ||
} | ||
|
||
/** | ||
* @throws Exception | ||
*/ | ||
public function __construct(RecursiveIterator $iterator, array $filter) | ||
{ | ||
parent::__construct($iterator); | ||
|
||
$this->filter = $filter; | ||
} | ||
|
||
public function accept(): bool | ||
{ | ||
$test = $this->getInnerIterator()->current(); | ||
|
||
if ($test instanceof TestSuite) { | ||
return true; | ||
} | ||
|
||
/** @var TestCase $test */ | ||
$testClass = get_class($test); | ||
|
||
if (!isset($this->filter[$testClass])) { | ||
return false; | ||
} | ||
|
||
$name = $test->getName(); | ||
|
||
return isset($this->filter[$testClass][$name]); | ||
} | ||
|
||
private static function extractTestCases(DOMXPath $xpath): Generator | ||
{ | ||
/** @var DOMElement $class */ | ||
foreach ($xpath->evaluate('/tests/testCaseClass') as $class) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as loading the XML-file for every new Iterator: please do this once and use a centralized lookup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is now addressed, see response to https://github.com/sebastianbergmann/phpunit/pull/4449/files#r503837936 |
||
$className = $class->getAttribute('name'); | ||
|
||
if (!$className) { | ||
continue; | ||
} | ||
|
||
/** @var DOMElement $method */ | ||
foreach ($xpath->evaluate('testCaseMethod', $class) as $method) { | ||
$methodName = $method->getAttribute('name'); | ||
|
||
if (!$methodName) { | ||
continue; | ||
} | ||
|
||
$dataSet = $method->getAttribute('dataSet'); | ||
|
||
yield [$className, $methodName, $dataSet]; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @return Generator<string> | ||
*/ | ||
private static function extractPhptFile(DOMXPath $xpath): Generator | ||
{ | ||
/* @var DOMElement $phptFile */ | ||
foreach ($xpath->evaluate('/tests/phptFile') as $phptFile) { | ||
$path = $phptFile->getAttribute('path'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: why does CodeCov mark these lines as not-reached? |
||
|
||
if ($path) { | ||
yield $path; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: why does CodeCov mark these lines as not-reached? |
||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,7 @@ | |
use PHPUnit\Runner\Filter\Factory; | ||
use PHPUnit\Runner\Filter\IncludeGroupFilterIterator; | ||
use PHPUnit\Runner\Filter\NameFilterIterator; | ||
use PHPUnit\Runner\Filter\XmlTestsIterator; | ||
use PHPUnit\Runner\Hook; | ||
use PHPUnit\Runner\NullTestResultCache; | ||
use PHPUnit\Runner\ResultCacheExtension; | ||
|
@@ -113,6 +114,7 @@ public function __construct(CodeCoverageFilter $filter = null) | |
/** | ||
* @throws \PHPUnit\Runner\Exception | ||
* @throws \PHPUnit\TextUI\XmlConfiguration\Exception | ||
* @throws \PHPUnit\Util\Xml\Exception | ||
* @throws Exception | ||
*/ | ||
public function run(TestSuite $suite, array $arguments = [], array $warnings = [], bool $exit = true): TestResult | ||
|
@@ -1046,13 +1048,17 @@ private function handleConfiguration(array &$arguments): void | |
$arguments['verbose'] = $arguments['verbose'] ?? false; | ||
} | ||
|
||
/** | ||
* @throws \PHPUnit\Util\Xml\Exception | ||
*/ | ||
private function processSuiteFilters(TestSuite $suite, array $arguments): void | ||
{ | ||
if (!$arguments['filter'] && | ||
empty($arguments['groups']) && | ||
empty($arguments['excludeGroups']) && | ||
empty($arguments['testsCovering']) && | ||
empty($arguments['testsUsing'])) { | ||
empty($arguments['testsUsing']) && | ||
empty($arguments['testsXml'])) { | ||
return; | ||
} | ||
|
||
|
@@ -1103,6 +1109,13 @@ static function (string $name): string { | |
); | ||
} | ||
|
||
if (!empty($arguments['testsXml'])) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Around here is the place to load+parse your configuration and get a list of tests. Perhaps you can even adapt/reuse the current |
||
$filterFactory->addFilter( | ||
new ReflectionClass(XmlTestsIterator::class), | ||
XmlTestsIterator::createFilterFromXmlFile($arguments['testsXml']), | ||
); | ||
} | ||
|
||
$suite->injectFilter($filterFactory); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
--TEST-- | ||
phpunit --list-tests-xml ../../_files/DataProviderTest.php | ||
--FILE-- | ||
<?php declare(strict_types=1); | ||
$xml = tempnam(sys_get_temp_dir(), __FILE__); | ||
file_put_contents($xml, <<<XML | ||
<?xml version="1.0"?> | ||
<tests> | ||
<!-- This class exists --> | ||
<testCaseClass name="PHPUnit\TestFixture\DataProviderTest"> | ||
<testCaseMethod name="testAdd" groups="default" dataSet="#0"/> | ||
<!-- This method does not exist --> | ||
<testCaseMethod name="methodDoesNotExist"/> | ||
<!-- name attribute missing --> | ||
<testCaseMethod /> | ||
</testCaseClass> | ||
|
||
<!-- name attribute missing --> | ||
<testCaseClass> | ||
<testCaseMethod name="testAdd" groups="default" dataSet="#0"/> | ||
</testCaseClass> | ||
|
||
<ignoredTag/> | ||
|
||
<testCaseClass name="Class\Does\Not\Exist"> | ||
<testCaseMethod name="methodAlsoDoesNotExist"/> | ||
</testCaseClass> | ||
</tests> | ||
XML | ||
); | ||
|
||
$_SERVER['argv'][1] = '--no-configuration'; | ||
$_SERVER['argv'][2] = '--tests-xml'; | ||
$_SERVER['argv'][3] = $xml; | ||
$_SERVER['argv'][4] = __DIR__ . '/../_files/DataProviderTest.php'; | ||
|
||
require __DIR__ . '/../bootstrap.php'; | ||
PHPUnit\TextUI\Command::main(false); | ||
|
||
unlink($xml); | ||
--EXPECTF-- | ||
PHPUnit %s by Sebastian Bergmann and contributors. | ||
|
||
. 1 / 1 (100%) | ||
|
||
Time: %s, Memory: %s | ||
|
||
OK (1 test, 1 assertion) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a way to ask anything that implements a
Test
its own unique identity, have a look at https://github.com/sebastianbergmann/phpunit/blob/master/src/Framework/Reorderable.php and the uses ofReorderable
.If you need more details about the (origins of) a test, let me know the use cases. It would be best if we can extend a central mechanism for identifying and locating tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to concentrate on this part first here before solving the others, might have an impact.
When a filter receives a test class in
accept()
, I've to figure out ifs part of the desired filter, i.e. the XML being feed back to phpunit.The format of the XML is defined via
\PHPUnit\Util\XmlTestListRenderer::render
phpunit/src/Util/XmlTestListRenderer.php
Line 42 in 1766543
and creates a XML structure like this:
And this now explains why I'm using
get_class()
and not something else: as a consumer of the XML, I've to match the producer.This is, for
\PHPUnit\Framework\TestCase
, further stipulated with thedataSet
-attribute, it's produced using this codephpunit/src/Util/XmlTestListRenderer.php
Lines 57 to 66 in 1766543
As can be seen, this manually removes some parts of
\PHPUnit\Framework\TestCase::getDataSetAsString
and writes it to the XML.For that reason, when reading the XML I'm reconstructing the original "data as string" with this:
so that in
accept()
I can just calland
getName
internally callsgetDataSetAsString
, so that in the end by calling:$testClass = get_class($test);
inaccept()
and doing
"{$methodName} with data set {$dataSet}
insetFilter
I've built the matching mirror logic for consuming what was produced.
I guess some of this "peekaboo" here is reflected in #4449 (comment)
So currently I don't see how e.g
\PHPUnit\Framework\TestCase::sortId
(of\PHPUnit\Framework\Reorderable
) helps me here, as the generated value is not usable in the context of what is produced in the XMLAs for the format used in the
\PHPUnit\Runner\Filter\XmlTestsIterator::$filter
, I tried to document it: