Files
farmOS-dev/vendor/brianium/paratest/src/Runners/PHPUnit/SuiteLoader.php
2024-12-10 15:08:16 +01:00

523 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace ParaTest\Runners\PHPUnit;
use ParaTest\Parser\NoClassInFileException;
use ParaTest\Parser\ParsedClass;
use ParaTest\Parser\Parser;
use PHPUnit\Framework\ExecutionOrderDependency;
use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\FilterMapper;
use PHPUnit\TextUI\XmlConfiguration\Configuration;
use PHPUnit\TextUI\XmlConfiguration\PhpHandler;
use PHPUnit\TextUI\XmlConfiguration\TestSuite;
use PHPUnit\Util\FileLoader;
use PHPUnit\Util\Test;
use ReflectionMethod;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer;
use SebastianBergmann\Environment\Runtime;
use SebastianBergmann\FileIterator\Facade;
use SebastianBergmann\Timer\Timer;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function array_filter;
use function array_intersect;
use function array_keys;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
use function assert;
use function count;
use function in_array;
use function is_array;
use function is_int;
use function ksort;
use function preg_match;
use function realpath;
use function sprintf;
use function strpos;
use function strrpos;
use function substr;
use function trim;
use function version_compare;
use const PHP_VERSION;
/** @internal */
final class SuiteLoader
{
/**
* The collection of loaded files.
*
* @var string[]
*/
private $files = [];
/** @var string[]|null */
private $suitesName = null;
/**
* The collection of parsed test classes.
*
* @var array<string, ExecutableTest>
*/
private $loadedSuites = [];
/**
* The configuration.
*
* @var Configuration|null
*/
private $configuration;
/** @var Options */
private $options;
/** @var OutputInterface */
private $output;
public function __construct(Options $options, OutputInterface $output)
{
$this->options = $options;
$this->configuration = $options->configuration();
$this->output = $output;
}
/**
* Returns all parsed suite objects as ExecutableTest
* instances.
*
* @return ExecutableTest[]
* @psalm-return list<ExecutableTest>
*/
public function getSuites(): array
{
return array_values($this->loadedSuites);
}
/**
* Returns a collection of TestMethod objects
* for all loaded ExecutableTest instances.
*
* @return TestMethod[]
*/
public function getTestMethods(): array
{
$methods = [];
foreach ($this->loadedSuites as $suite) {
assert($suite instanceof Suite);
$methods = array_merge($methods, $suite->getFunctions());
}
return $methods;
}
/**
* Populates the loaded suite collection. Will load suites
* based off a phpunit xml configuration or a specified path.
*
* @throws RuntimeException
*/
public function load(): void
{
$this->loadConfiguration();
$testSuiteCollection = null;
if (($path = $this->options->path()) !== null) {
if (realpath($path) === false) {
throw new RuntimeException("Invalid path {$path} provided");
}
$this->files = array_merge(
$this->files,
(new Facade())->getFilesAsArray($path, ['Test.php']),
);
} elseif (
$this->options->parallelSuite()
&& $this->configuration !== null
&& ! $this->configuration->testSuite()->isEmpty()
) {
$testSuiteCollection = $this->configuration->testSuite()->asArray();
$this->suitesName = array_map(static function (TestSuite $testSuite): string {
return $testSuite->name();
}, $testSuiteCollection);
} elseif (
$this->configuration !== null
&& ! $this->configuration->testSuite()->isEmpty()
) {
$testSuiteCollection = array_filter(
$this->configuration->testSuite()->asArray(),
function (TestSuite $testSuite): bool {
return $this->options->testsuite() === [] ||
in_array($testSuite->name(), $this->options->testsuite(), true);
},
);
foreach ($testSuiteCollection as $testSuite) {
$this->loadFilesFromTestSuite($testSuite);
}
}
if ($path === null && $testSuiteCollection === null) {
throw new RuntimeException('No path or configuration provided (tests must end with Test.php)');
}
$this->files = array_unique($this->files); // remove duplicates
$this->initSuites();
$this->warmCoverageCache();
}
/**
* Called after all files are loaded. Parses loaded files into
* ExecutableTest objects - either Suite or TestMethod or FullSuite.
*/
private function initSuites(): void
{
if (is_array($this->suitesName)) {
foreach ($this->suitesName as $suiteName) {
$this->loadedSuites[$suiteName] = $this->createFullSuite($suiteName);
}
} else {
// The $class->getParentsCount() + array_merge(...$loadedSuites) stuff
// are needed to run test with child tests early, because PHPUnit autoloading
// of such classes in WrapperRunner environments fails (Runner is fine)
$loadedSuites = [];
foreach ($this->files as $path) {
try {
$class = (new Parser($path))->getClass();
$suite = $this->createSuite($path, $class);
if (count($suite->getFunctions()) > 0) {
$loadedSuites[$class->getParentsCount()][$path] = $suite;
}
} catch (NoClassInFileException $e) {
continue;
}
}
foreach (array_keys($loadedSuites) as $key) {
ksort($loadedSuites[$key]);
}
ksort($loadedSuites);
foreach ($loadedSuites as $loadedSuite) {
$this->loadedSuites = array_merge($this->loadedSuites, $loadedSuite);
}
}
}
/**
* @return TestMethod[]
* @psalm-return list<TestMethod>
*/
private function executableTests(string $path, ParsedClass $class): array
{
$executableTests = [];
$methodBatches = $this->getMethodBatches($class);
foreach ($methodBatches as $methodBatch) {
$executableTests[] = new TestMethod(
$path,
$methodBatch,
$this->options->hasCoverage(),
$this->options->needsTeamcity(),
$this->options->tmpDir(),
);
}
return $executableTests;
}
/**
* Get method batches.
*
* Identify method dependencies, and group dependents and dependees on a single methodBatch.
* Use max batch size to fill batches.
*
* @return string[][] of MethodBatches. Each MethodBatch has an array of method names
*/
private function getMethodBatches(ParsedClass $class): array
{
$classMethods = $class->getMethods();
$maxBatchSize = $this->options->functional() ? $this->options->maxBatchSize() : 0;
assert($maxBatchSize !== null);
$batches = [];
foreach ($classMethods as $method) {
$tests = $this->getMethodTests($class, $method);
// if filter passed to paratest then method tests can be blank if not match to filter
if (count($tests) === 0) {
continue;
}
$dependencies = Test::getDependencies($class->getName(), $method->getName());
if (count($dependencies) !== 0) {
$this->addDependentTestsToBatchSet($batches, $dependencies, $tests);
} else {
$this->addTestsToBatchSet($batches, $tests, $maxBatchSize);
}
}
return $batches;
}
/**
* @param string[][] $batches
* @param ExecutionOrderDependency[] $dependencies
* @param string[] $tests
*/
private function addDependentTestsToBatchSet(array &$batches, array $dependencies, array $tests): void
{
$dependencies = array_map(static function (ExecutionOrderDependency $dependency): string {
return substr($dependency->getTarget(), (int) strrpos($dependency->getTarget(), ':') + 1);
}, $dependencies);
foreach ($batches as $key => $batch) {
foreach ($batch as $methodName) {
if (in_array($methodName, $dependencies, true)) {
$batches[$key] = array_merge($batches[$key], $tests);
continue;
}
}
}
}
/**
* @param string[][] $batches
* @param string[] $tests
*/
private function addTestsToBatchSet(array &$batches, array $tests, int $maxBatchSize): void
{
foreach ($tests as $test) {
$lastIndex = count($batches) - 1;
if (
$lastIndex !== -1
&& count($batches[$lastIndex]) < $maxBatchSize
) {
$batches[$lastIndex][] = $test;
} else {
$batches[] = [$test];
}
}
}
/**
* Get method all available tests.
*
* With empty filter this method returns single test if doesn't have data provider or
* data provider is not used and return all test if has data provider and data provider is used.
*
* @return string[] array of test names
* @psalm-return list<string>
*/
private function getMethodTests(ParsedClass $class, ReflectionMethod $method): array
{
$result = [];
/** @var string[] $groups */
$groups = Test::getGroups($class->getName(), $method->getName());
if ($this->containsOnlyVirtualGroups($groups)) {
$groups[] = 'default';
}
if (! $this->testMatchGroupOptions($groups)) {
return $result;
}
try {
$providedData = Test::getProvidedData($class->getName(), $method->getName());
} catch (Throwable $throwable) {
$providedData = null;
}
if ($providedData !== null) {
foreach (array_keys($providedData) as $key) {
$test = sprintf(
'%s with data set %s',
$method->getName(),
is_int($key) ? '#' . $key : '"' . $key . '"',
);
if (! $this->testMatchFilterOptions($class->getName(), $test)) {
continue;
}
$result[] = $test;
}
} elseif ($this->testMatchFilterOptions($class->getName(), $method->getName())) {
$result = [$method->getName()];
}
return $result;
}
/** @param string[] $groups */
private function testMatchGroupOptions(array $groups): bool
{
if ($this->options->group() === [] && $this->options->excludeGroup() === []) {
return true;
}
$matchGroupIncluded = (
$this->options->group() !== []
&& array_intersect($groups, $this->options->group()) !== []
);
$matchGroupNotExcluded = (
$this->options->excludeGroup() !== []
&& array_intersect($groups, $this->options->excludeGroup()) === []
);
return $matchGroupIncluded || $matchGroupNotExcluded;
}
private function testMatchFilterOptions(string $className, string $name): bool
{
if (($filter = $this->options->filter()) === null) {
return true;
}
$re = '/' . trim($filter, '/') . '/';
$fullName = $className . '::' . $name;
return preg_match($re, $fullName) === 1;
}
private function createSuite(string $path, ParsedClass $class): Suite
{
return new Suite(
$path,
$this->executableTests(
$path,
$class,
),
$this->options->hasCoverage(),
$this->options->needsTeamcity(),
$this->options->tmpDir(),
);
}
private function createFullSuite(string $suiteName): FullSuite
{
return new FullSuite(
$suiteName,
$this->options->hasCoverage(),
$this->options->needsTeamcity(),
$this->options->tmpDir(),
);
}
/** @see \PHPUnit\TextUI\XmlConfiguration\TestSuiteMapper::map */
private function loadFilesFromTestSuite(TestSuite $testSuiteCollection): void
{
foreach ($testSuiteCollection->directories() as $directory) {
if (
! version_compare(
PHP_VERSION,
$directory->phpVersion(),
$directory->phpVersionOperator()->asString(),
)
) {
continue; // @codeCoverageIgnore
}
$exclude = [];
foreach ($testSuiteCollection->exclude()->asArray() as $file) {
$exclude[] = $file->path();
}
$this->files = array_merge($this->files, (new Facade())->getFilesAsArray(
$directory->path(),
$directory->suffix(),
$directory->prefix(),
$exclude,
));
}
foreach ($testSuiteCollection->files() as $file) {
if (
! version_compare(
PHP_VERSION,
$file->phpVersion(),
$file->phpVersionOperator()->asString(),
)
) {
continue; // @codeCoverageIgnore
}
$this->files[] = $file->path();
}
}
private function loadConfiguration(): void
{
if ($this->configuration !== null) {
(new PhpHandler())->handle($this->configuration->php());
}
$bootstrap = null;
if ($this->options->bootstrap() !== null) {
$bootstrap = $this->options->bootstrap();
} elseif ($this->configuration !== null && $this->configuration->phpunit()->hasBootstrap()) {
$bootstrap = $this->configuration->phpunit()->bootstrap();
}
if ($bootstrap === null) {
return;
}
FileLoader::checkAndLoad($bootstrap);
}
private function warmCoverageCache(): void
{
if (
! $this->options->hasCoverage()
|| ! (new Runtime())->canCollectCodeCoverage()
|| ($configuration = $this->options->configuration()) === null
|| ! $configuration->codeCoverage()->hasCacheDirectory()
) {
return;
}
$filter = new Filter();
(new FilterMapper())->map(
$filter,
$configuration->codeCoverage(),
);
$timer = new Timer();
$timer->start();
$this->output->write('Warming cache for static analysis ... ');
(new CacheWarmer())->warmCache(
$configuration->codeCoverage()->cacheDirectory()->path(),
! $configuration->codeCoverage()->disableCodeCoverageIgnore(),
$configuration->codeCoverage()->ignoreDeprecatedCodeUnits(),
$filter,
);
$this->output->write(sprintf("done [%s]\n\n", $timer->stop()->asString()));
}
/**
* @see \PHPUnit\Framework\TestSuite::containsOnlyVirtualGroups
*
* @param string[] $groups
*/
private function containsOnlyVirtualGroups(array $groups): bool
{
foreach ($groups as $group) {
if (strpos($group, '__phpunit_') !== 0) {
return false;
}
}
return true;
}
}