Merge branch '2.x' into dataset-arguments-check

This commit is contained in:
Nuno Maduro
2023-03-21 21:10:22 +00:00
committed by GitHub
56 changed files with 550 additions and 648 deletions

View File

@ -23,6 +23,7 @@ final class BootOverrides implements Bootstrapper
'Runner/TestSuiteLoader.php',
'TextUI/Command/WarmCodeCoverageCacheCommand.php',
'TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php',
'TextUI/TestSuiteFilterProcessor.php',
];
/**

View File

@ -21,7 +21,6 @@ final class BootSubscribers implements Bootstrapper
* @var array<int, class-string<Subscriber>>
*/
private const SUBSCRIBERS = [
Subscribers\EnsureConfigurationIsValid::class,
Subscribers\EnsureConfigurationIsAvailable::class,
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
Subscribers\EnsureKernelDumpIsFlushed::class,
@ -46,9 +45,7 @@ final class BootSubscribers implements Bootstrapper
assert($instance instanceof Subscriber);
Event\Facade::registerSubscriber(
$instance
);
Event\Facade::instance()->registerSubscriber($instance);
}
}
}

View File

@ -307,7 +307,7 @@ trait Testable
*/
public static function getPrintableTestCaseName(): string
{
return ltrim(self::class, 'P\\');
return preg_replace('/P\\\/', '', self::class, 1);
}
/**

View File

@ -1,113 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest;
use Pest\Support\Str;
use SimpleXMLElement;
use Throwable;
/**
* @internal
*/
final class ConfigLoader
{
/**
* Default path if config loading went wrong.
*
* @var string
*/
public const DEFAULT_TESTS_PATH = 'tests';
/**
* XML tree of the PHPUnit configuration file.
*/
private ?SimpleXMLElement $config = null;
/**
* Creates a new instance of the config loader.
*/
public function __construct(private readonly string $rootPath)
{
$this->loadConfiguration();
}
/**
* Get the tests directory or fallback to default path.
*/
public function getTestsDirectory(): string
{
$suiteDirectory = [];
if (is_null($this->config)) {
return self::DEFAULT_TESTS_PATH;
}
$suiteDirectory = $this->config->xpath('/phpunit/testsuites/testsuite/directory');
if ($suiteDirectory === []) {
return self::DEFAULT_TESTS_PATH;
}
$directory = (string) ($suiteDirectory[0] ?? '');
if ($directory === '') {
return self::DEFAULT_TESTS_PATH;
}
// Return the whole directory if only a separator found (e.g. `./tests`)
if (substr_count($directory, DIRECTORY_SEPARATOR) === 1) {
return is_dir($directory) ? $directory : self::DEFAULT_TESTS_PATH;
}
$basePath = Str::beforeLast($directory, DIRECTORY_SEPARATOR);
return is_dir($basePath) ? $basePath : self::DEFAULT_TESTS_PATH;
}
/**
* Get the configuration file path.
*/
public function getConfigurationFilePath(): string|bool
{
$candidates = [
$this->rootPath.'/phpunit.xml',
$this->rootPath.'/phpunit.dist.xml',
$this->rootPath.'/phpunit.xml.dist',
];
foreach ($candidates as $candidate) {
if (is_file($candidate)) {
return realpath($candidate);
}
}
return false;
}
/**
* Load the configuration file.
*/
private function loadConfiguration(): void
{
$configPath = $this->getConfigurationFilePath();
if (is_bool($configPath)) {
return;
}
$oldReportingLevel = error_reporting(0);
$content = file_get_contents($configPath);
if ($content !== false) {
try {
$this->config = new SimpleXMLElement($content);
} catch (Throwable) { // @phpstan-ignore-line
// @ignoreException
}
}
// Restore the correct error reporting
error_reporting($oldReportingLevel);
}
}

View File

@ -19,6 +19,6 @@ final class AfterAllAlreadyExist extends InvalidArgumentException implements Exc
*/
public function __construct(string $filename)
{
parent::__construct(sprintf('The afterAll already exist in the filename `%s`.', $filename));
parent::__construct(sprintf('The afterAll already exists in the filename `%s`.', $filename));
}
}

View File

@ -19,6 +19,6 @@ final class AfterEachAlreadyExist extends InvalidArgumentException implements Ex
*/
public function __construct(string $filename)
{
parent::__construct(sprintf('The afterEach already exist in the filename `%s`.', $filename));
parent::__construct(sprintf('The afterEach already exists in the filename `%s`.', $filename));
}
}

View File

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class AttributeNotSupportedYet extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new Exception instance.
*/
public function __construct(string $attribute, string $value)
{
parent::__construct(sprintf('The PHPUnit attribute `%s` with value `%s` is not supported yet.', $attribute, $value));
}
}

View File

@ -19,6 +19,6 @@ final class BeforeEachAlreadyExist extends InvalidArgumentException implements E
*/
public function __construct(string $filename)
{
parent::__construct(sprintf('The beforeEach already exist in the filename `%s`.', $filename));
parent::__construct(sprintf('The beforeEach already exists in the filename `%s`.', $filename));
}
}

View File

@ -19,6 +19,6 @@ final class DatasetAlreadyExists extends InvalidArgumentException implements Exc
*/
public function __construct(string $name, string $scope)
{
parent::__construct(sprintf('A dataset with the name `%s` already exist in scope [%s].', $name, $scope));
parent::__construct(sprintf('A dataset with the name `%s` already exists in scope [%s].', $name, $scope));
}
}

View File

@ -19,6 +19,6 @@ final class FileOrFolderNotFound extends InvalidArgumentException implements Exc
*/
public function __construct(string $filename)
{
parent::__construct(sprintf('The file or folder with the name `%s` not found.', $filename));
parent::__construct(sprintf('The file or folder with the name `%s` could not be found.', $filename));
}
}

View File

@ -19,6 +19,6 @@ final class TestAlreadyExist extends InvalidArgumentException implements Excepti
*/
public function __construct(string $fileName, string $description)
{
parent::__construct(sprintf('A test with the description `%s` already exist in the filename `%s`.', $description, $fileName));
parent::__construct(sprintf('A test with the description `%s` already exists in the filename `%s`.', $description, $fileName));
}
}

View File

@ -19,6 +19,6 @@ final class TestDescriptionMissing extends InvalidArgumentException implements E
*/
public function __construct(string $fileName)
{
parent::__construct(sprintf('Test misses description in the filename `%s`.', $fileName));
parent::__construct(sprintf('Test description is missing in the filename `%s`.', $fileName));
}
}

View File

@ -64,7 +64,7 @@ final class Expectation
*/
public function and(mixed $value): Expectation
{
return $value instanceof static ? $value : new self($value);
return $value instanceof self ? $value : new self($value);
}
/**

View File

@ -11,7 +11,9 @@ use Pest\Plugins\Actions\CallsBoot;
use Pest\Plugins\Actions\CallsHandleArguments;
use Pest\Plugins\Actions\CallsShutdown;
use Pest\Support\Container;
use PHPUnit\TestRunner\TestResult\Facade;
use PHPUnit\TextUI\Application;
use PHPUnit\TextUI\Configuration\Registry;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -90,8 +92,11 @@ final class Kernel
]);
}
$configuration = Registry::get();
$result = Facade::result();
return CallsAddsOutput::execute(
Result::exitCode(),
Result::exitCode($configuration, $result),
);
}

View File

@ -224,7 +224,7 @@ final class TeamCityLogger
*/
private function registerSubscribers(): void
{
Facade::registerSubscribers(
$subscribers = [
new TestSuiteStartedSubscriber($this),
new TestSuiteFinishedSubscriber($this),
new TestPreparedSubscriber($this),
@ -235,7 +235,9 @@ final class TeamCityLogger
new TestSkippedSubscriber($this),
new TestConsideredRiskySubscriber($this),
new TestExecutionFinishedSubscriber($this),
);
];
Facade::instance()->registerSubscribers(...$subscribers);
}
private function setFlowId(): void

View File

@ -124,7 +124,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeGreaterThan(int|float $expected, string $message = ''): self
public function toBeGreaterThan(int|float|\DateTime|\DateTimeImmutable $expected, string $message = ''): self
{
Assert::assertGreaterThan($expected, $this->value, $message);
@ -136,7 +136,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeGreaterThanOrEqual(int|float $expected, string $message = ''): self
public function toBeGreaterThanOrEqual(int|float|\DateTime|\DateTimeImmutable $expected, string $message = ''): self
{
Assert::assertGreaterThanOrEqual($expected, $this->value, $message);
@ -148,7 +148,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeLessThan(int|float $expected, string $message = ''): self
public function toBeLessThan(int|float|\DateTime|\DateTimeImmutable $expected, string $message = ''): self
{
Assert::assertLessThan($expected, $this->value, $message);
@ -160,7 +160,7 @@ final class Expectation
*
* @return self<TValue>
*/
public function toBeLessThanOrEqual(int|float $expected, string $message = ''): self
public function toBeLessThanOrEqual(int|float|\DateTime|\DateTimeImmutable $expected, string $message = ''): self
{
Assert::assertLessThanOrEqual($expected, $this->value, $message);

View File

@ -10,6 +10,7 @@ use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\Covers\CoversNothing;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Plugins\Only;
use Pest\Support\Backtrace;
use Pest\Support\Exporter;
use Pest\Support\HigherOrderCallables;
@ -134,6 +135,16 @@ final class TestCall
return $this;
}
/**
* Filters the test suite by "only" tests.
*/
public function only(): self
{
Only::enable($this);
return $this;
}
/**
* Skips the current test.
*/

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '2.x-dev';
return '2.1.0';
}
function testDirectory(string $file = ''): string

View File

@ -22,7 +22,8 @@ final class Bail implements HandlesArguments
if ($this->hasArgument('--bail', $arguments)) {
$arguments = $this->popArgument('--bail', $arguments);
$arguments = $this->pushArgument('--stop-on-defect', $arguments);
$arguments = $this->pushArgument('--stop-on-failure', $arguments);
$arguments = $this->pushArgument('--stop-on-error', $arguments);
}
return $arguments;

View File

@ -102,6 +102,13 @@ final class Help implements HandlesArguments
'desc' => 'Initialise a standard Pest configuration',
]], ...$content['Configuration']];
$content['Execution'] = [...[
[
'arg' => '--parallel',
'desc' => 'Run tests in parallel',
],
], ...$content['Execution']];
$content['Selection'] = array_merge([
[
'arg' => '--bail',

61
src/Plugins/Only.php Normal file
View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\Shutdownable;
use Pest\PendingCalls\TestCall;
/**
* @internal
*/
final class Only implements Shutdownable
{
/**
* The temporary folder.
*/
private const TEMPORARY_FOLDER = __DIR__
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
.'..'
.DIRECTORY_SEPARATOR
.'.temp';
/**
* {@inheritDoc}
*/
public function shutdown(): void
{
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (file_exists($lockFile)) {
unlink($lockFile);
}
}
/**
* Creates the lock file.
*/
public static function enable(TestCall $testCall): void
{
$testCall->group('__pest_only');
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
if (! file_exists($lockFile)) {
touch($lockFile);
}
}
/**
* Checks if "only" mode is enabled.
*/
public static function isEnabled(): bool
{
$lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock';
return file_exists($lockFile);
}
}

View File

@ -32,7 +32,7 @@ final class Parallel implements HandlesArguments
/**
* @var string[]
*/
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--retry'];
private const UNSUPPORTED_ARGUMENTS = ['--todos', '--retry'];
/**
* Whether the given command line arguments indicate that the test suite should be run in parallel.

View File

@ -20,13 +20,13 @@ use ParaTest\Options;
use ParaTest\RunnerInterface;
use ParaTest\WrapperRunner\SuiteLoader;
use ParaTest\WrapperRunner\WrapperWorker;
use Pest\Result;
use Pest\TestSuite;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\TextUI\ShellExitCodeCalculator;
use PHPUnit\Util\ExcludeList;
use function realpath;
use SebastianBergmann\Timer\Timer;
@ -114,7 +114,8 @@ final class WrapperRunner implements RunnerInterface
ExcludeList::addDirectory($directory);
TestResultFacade::init();
EventFacade::seal();
EventFacade::instance()->seal();
$suiteLoader = new SuiteLoader($this->options, $this->output, $this->codeCoverageFilterRegistry);
$this->pending = $this->getTestFiles($suiteLoader);
@ -329,14 +330,7 @@ final class WrapperRunner implements RunnerInterface
$this->generateCodeCoverageReports();
$this->generateLogs();
$exitCode = (new ShellExitCodeCalculator())->calculate(
$this->options->configuration->failOnEmptyTestSuite(),
$this->options->configuration->failOnRisky(),
$this->options->configuration->failOnWarning(),
$this->options->configuration->failOnIncomplete(),
$this->options->configuration->failOnSkipped(),
$testResultSum,
);
$exitCode = Result::exitCode($this->options->configuration, $testResultSum);
$this->clearFiles($this->testresultFiles);
$this->clearFiles($this->coverageFiles);
@ -354,7 +348,10 @@ final class WrapperRunner implements RunnerInterface
}
$coverageManager = new CodeCoverage();
$coverageManager->init($this->options->configuration, $this->codeCoverageFilterRegistry);
// @phpstan-ignore-next-line
is_bool(true) && $coverageManager->init($this->options->configuration, $this->codeCoverageFilterRegistry, true);
$coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
foreach ($this->coverageFiles as $coverageFile) {
$coverageMerger->addCoverageFromFile($coverageFile);

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Exceptions\InvalidOption;
/**
* @internal
*/
final class ProcessIsolation implements HandlesArguments
{
use Concerns\HandleArguments;
/**
* {@inheritDoc}
*/
public function handleArguments(array $arguments): array
{
if ($this->hasArgument('--process-isolation', $arguments)) {
throw new InvalidOption('The [--process-isolation] option is not supported.');
}
return $arguments;
}
}

32
src/Plugins/Profile.php Normal file
View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Exceptions\InvalidOption;
/**
* @internal
*/
final class Profile implements HandlesArguments
{
use Concerns\HandleArguments;
/**
* {@inheritDoc}
*/
public function handleArguments(array $arguments): array
{
if (! $this->hasArgument('--profile', $arguments)) {
return $arguments;
}
if ($this->hasArgument('--parallel', $arguments)) {
throw new InvalidOption('The [--profile] option is not supported when running in parallel.');
}
return $arguments;
}
}

View File

@ -23,7 +23,6 @@ final class Retry implements HandlesArguments
return $arguments;
}
// If running in parallel, we need to disable the retry plugin
if ($this->hasArgument('--parallel', $arguments)) {
throw new InvalidOption('The [--retry] option is not supported when running in parallel.');
}

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Pest;
use PHPUnit\TestRunner\TestResult\Facade;
use PHPUnit\TextUI\Configuration\Registry;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\Configuration;
/**
* @internal
@ -21,26 +21,24 @@ final class Result
/**
* If the exit code is different from 0.
*/
public static function failed(): bool
public static function failed(Configuration $configuration, TestResult $result): bool
{
return ! self::ok();
return ! self::ok($configuration, $result);
}
/**
* If the exit code is exactly 0.
*/
public static function ok(): bool
public static function ok(Configuration $configuration, TestResult $result): bool
{
return self::exitCode() === self::SUCCESS_EXIT;
return self::exitCode($configuration, $result) === self::SUCCESS_EXIT;
}
/**
* Get the test execution's exit code.
*/
public static function exitCode(): int
public static function exitCode(Configuration $configuration, TestResult $result): int
{
$result = Facade::result();
$returnCode = self::FAILURE_EXIT;
if ($result->wasSuccessfulIgnoringPhpunitWarnings()
@ -48,8 +46,6 @@ final class Result
$returnCode = self::SUCCESS_EXIT;
}
$configuration = Registry::get();
if ($configuration->failOnEmptyTestSuite() && $result->numberOfTests() === 0) {
$returnCode = self::FAILURE_EXIT;
}

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Subscribers;
use Pest\Exceptions\AttributeNotSupportedYet;
use PHPUnit\Event\TestRunner\Configured;
use PHPUnit\Event\TestRunner\ConfiguredSubscriber;
/**
* @internal
*/
final class EnsureConfigurationIsValid implements ConfiguredSubscriber
{
/**
* Runs the subscriber.
*/
public function notify(Configured $event): void
{
$configuration = $event->configuration();
if ($configuration->processIsolation()) {
throw new AttributeNotSupportedYet('processIsolation', 'true');
}
}
}

View File

@ -73,7 +73,7 @@ final class Arr
foreach ($array as $key => $value) {
if (is_array($value) && $value !== []) {
$results = array_merge($results, static::dot($value, $prepend.$key.'.'));
$results = array_merge($results, self::dot($value, $prepend.$key.'.'));
} else {
$results[$prepend.$value] = $value;
}

View File

@ -42,7 +42,7 @@ final class Reflection
}
if (is_callable($method)) {
return static::bindCallable($method, $args);
return self::bindCallable($method, $args);
}
throw $exception;
@ -72,7 +72,7 @@ final class Reflection
return $test instanceof \PHPUnit\Framework\TestCase
? Closure::fromCallable($callable)->bindTo($test)(...$test->providedData())
: static::bindCallable($callable);
: self::bindCallable($callable);
}
/**

View File

@ -61,7 +61,8 @@ final class Str
{
$code = self::PREFIX.str_replace(' ', '_', $code);
return (string) preg_replace('/[^A-Z_a-z0-9]/', '_', $code);
// sticks to PHP8.2 function naming rules https://www.php.net/manual/en/functions.user-defined.php
return (string) preg_replace('/[^a-zA-Z0-9_\x80-\xff]/', '_', $code);
}
/**

View File

@ -62,7 +62,10 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter
$dirtyFiles = array_map(fn ($file, $status): string => in_array($status, ['R', 'RM'], true) ? explode(' -> ', $file)[1] : $file, array_keys($dirtyFiles), $dirtyFiles);
$dirtyFiles = array_filter($dirtyFiles, fn ($file): bool => str_starts_with('.'.DIRECTORY_SEPARATOR.$file, TestSuite::getInstance()->testPath));
$dirtyFiles = array_filter(
$dirtyFiles,
fn ($file): bool => str_starts_with('.'.DIRECTORY_SEPARATOR.$file, TestSuite::getInstance()->testPath) || str_starts_with($file, TestSuite::getInstance()->testPath)
);
$dirtyFiles = array_values($dirtyFiles);