diff --git a/bin/pest b/bin/pest index d6bcc1b4..f73a728d 100755 --- a/bin/pest +++ b/bin/pest @@ -13,39 +13,39 @@ use Symfony\Component\Console\Output\ConsoleOutput; // Ensures Collision's Printer is registered. $_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter'; - $args = $_SERVER['argv']; + $arguments = $originalArguments = $_SERVER['argv']; $dirty = false; $todo = false; - foreach ($args as $key => $value) { + foreach ($arguments as $key => $value) { if ($value === '--compact') { $_SERVER['COLLISION_PRINTER_COMPACT'] = 'true'; - unset($args[$key]); + unset($arguments[$key]); } if ($value === '--profile') { $_SERVER['COLLISION_PRINTER_PROFILE'] = 'true'; - unset($args[$key]); + unset($arguments[$key]); } if (str_contains($value, '--test-directory')) { - unset($args[$key]); + unset($arguments[$key]); } if ($value === '--dirty') { $dirty = true; - unset($args[$key]); + unset($arguments[$key]); } if (in_array($value, ['--todo', '--todos'], true)) { $todo = true; - unset($args[$key]); + unset($arguments[$key]); } if (str_contains($value, '--teamcity')) { - unset($args[$key]); - $args[] = '--no-output'; + unset($arguments[$key]); + $arguments[] = '--no-output'; unset($_SERVER['COLLISION_PRINTER']); } } @@ -88,9 +88,9 @@ use Symfony\Component\Console\Output\ConsoleOutput; try { $kernel = Kernel::boot($testSuite, $input, $output); - $result = $kernel->handle($args); + $result = $kernel->handle($originalArguments, $arguments); - $kernel->shutdown(); + $kernel->terminate(); } catch (Throwable|Error $e) { Panic::with($e); } diff --git a/composer.json b/composer.json index 94ebf1d8..4af18284 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "require-dev": { "pestphp/pest-dev-tools": "^3.0.0", "pestphp/pest-plugin-type-coverage": "^3.0.0", - "symfony/process": "^7.0.2" + "symfony/process": "^7.0.3" }, "minimum-stability": "dev", "prefer-stable": true, @@ -105,8 +105,7 @@ "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", - "Pest\\Plugins\\Parallel", - "Pest\\Plugins\\JUnit" + "Pest\\Plugins\\Parallel" ] }, "phpstan": { diff --git a/docker/Dockerfile b/docker/Dockerfile index a2fa59c1..c372469b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,13 +2,11 @@ ARG PHP=8.1 FROM php:${PHP}-cli-alpine RUN apk update && apk add \ - zip libzip-dev icu-dev git \ + zip libzip-dev icu-dev git -RUN docker-php-ext-configure zip intl RUN docker-php-ext-install zip intl -RUN docker-php-ext-enable zip intl -RUN apk add --no-cache linux-headers +RUN apk add --no-cache linux-headers autoconf build-base RUN pecl install xdebug RUN docker-php-ext-enable xdebug COPY --from=composer:2 /usr/bin/composer /usr/bin/composer diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index bc0e0953..a1ca5ff5 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -1,16 +1,459 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace PHPUnit\Logging\JUnit; +use DOMDocument; +use DOMElement; +use PHPUnit\Event\Code\Test; +use PHPUnit\Event\Code\TestMethod; +use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; +use PHPUnit\Event\InvalidArgumentException; +use PHPUnit\Event\Telemetry\HRTime; +use PHPUnit\Event\Telemetry\Info; +use PHPUnit\Event\Test\Errored; +use PHPUnit\Event\Test\Failed; +use PHPUnit\Event\Test\Finished; +use PHPUnit\Event\Test\MarkedIncomplete; +use PHPUnit\Event\Test\PreparationStarted; +use PHPUnit\Event\Test\Prepared; +use PHPUnit\Event\Test\Skipped; +use PHPUnit\Event\TestSuite\Started; +use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\TextUI\Output\Printer; +use PHPUnit\Util\Xml; +use function assert; +use function basename; +use function is_int; +use function sprintf; +use function str_replace; +use function trim; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ final class JunitXmlLogger { + private readonly Printer $printer; + + private readonly \Pest\Logging\Converter $converter; // pest-added + + private DOMDocument $document; + + private DOMElement $root; + + /** + * @var DOMElement[] + */ + private array $testSuites = []; + + /** + * @psalm-var array + */ + private array $testSuiteTests = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteAssertions = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteErrors = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteFailures = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteSkipped = [0]; + + /** + * @psalm-var array + */ + private array $testSuiteTimes = [0]; + + private int $testSuiteLevel = 0; + + private ?DOMElement $currentTestCase = null; + + private ?HRTime $time = null; + + private bool $prepared = false; + + private bool $preparationFailed = false; + + /** + * @throws EventFacadeIsSealedException + * @throws UnknownSubscriberTypeException + */ public function __construct(Printer $printer, Facade $facade) { - /** @see \Pest\Logging\JUnit\JUnitLogger */ + $this->printer = $printer; + $this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added + + $this->registerSubscribers($facade); + $this->createDocument(); + } + + public function flush(): void + { + $this->printer->print($this->document->saveXML()); + + $this->printer->flush(); + } + + public function testSuiteStarted(Started $event): void + { + $testSuite = $this->document->createElement('testsuite'); + $testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); // pest-changed + + if ($event->testSuite()->isForTestClass()) { + $testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); // pest-changed + } + + if ($this->testSuiteLevel > 0) { + $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); + } else { + $this->root->appendChild($testSuite); + } + + $this->testSuiteLevel++; + $this->testSuites[$this->testSuiteLevel] = $testSuite; + $this->testSuiteTests[$this->testSuiteLevel] = 0; + $this->testSuiteAssertions[$this->testSuiteLevel] = 0; + $this->testSuiteErrors[$this->testSuiteLevel] = 0; + $this->testSuiteFailures[$this->testSuiteLevel] = 0; + $this->testSuiteSkipped[$this->testSuiteLevel] = 0; + $this->testSuiteTimes[$this->testSuiteLevel] = 0; + } + + public function testSuiteFinished(): void + { + $this->testSuites[$this->testSuiteLevel]->setAttribute( + 'tests', + (string) $this->testSuiteTests[$this->testSuiteLevel], + ); + + $this->testSuites[$this->testSuiteLevel]->setAttribute( + 'assertions', + (string) $this->testSuiteAssertions[$this->testSuiteLevel], + ); + + $this->testSuites[$this->testSuiteLevel]->setAttribute( + 'errors', + (string) $this->testSuiteErrors[$this->testSuiteLevel], + ); + + $this->testSuites[$this->testSuiteLevel]->setAttribute( + 'failures', + (string) $this->testSuiteFailures[$this->testSuiteLevel], + ); + + $this->testSuites[$this->testSuiteLevel]->setAttribute( + 'skipped', + (string) $this->testSuiteSkipped[$this->testSuiteLevel], + ); + + $this->testSuites[$this->testSuiteLevel]->setAttribute( + 'time', + sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]), + ); + + if ($this->testSuiteLevel > 1) { + $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; + $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; + $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; + $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; + $this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel]; + $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; + } + + $this->testSuiteLevel--; + } + + /** + * @throws InvalidArgumentException + */ + public function testPreparationStarted(PreparationStarted $event): void + { + $this->createTestCase($event); + } + + /** + * @throws InvalidArgumentException + */ + public function testPreparationFailed(): void + { + $this->preparationFailed = true; + } + + /** + * @throws InvalidArgumentException + */ + public function testPrepared(): void + { + $this->prepared = true; + } + + /** + * @throws InvalidArgumentException + */ + public function testFinished(Finished $event): void + { + if ($this->preparationFailed) { + return; + } + + $this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed()); + } + + /** + * @throws InvalidArgumentException + */ + public function testMarkedIncomplete(MarkedIncomplete $event): void + { + $this->handleIncompleteOrSkipped($event); + } + + /** + * @throws InvalidArgumentException + */ + public function testSkipped(Skipped $event): void + { + $this->handleIncompleteOrSkipped($event); + } + + /** + * @throws InvalidArgumentException + */ + public function testErrored(Errored $event): void + { + $this->handleFault($event, 'error'); + + $this->testSuiteErrors[$this->testSuiteLevel]++; + } + + /** + * @throws InvalidArgumentException + */ + public function testFailed(Failed $event): void + { + $this->handleFault($event, 'failure'); + + $this->testSuiteFailures[$this->testSuiteLevel]++; + } + + /** + * @throws InvalidArgumentException + */ + private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void + { + assert($this->currentTestCase !== null); + assert($this->time !== null); + + $time = $telemetryInfo->time()->duration($this->time)->asFloat(); + + $this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed; + + $this->currentTestCase->setAttribute( + 'assertions', + (string) $numberOfAssertionsPerformed, + ); + + $this->currentTestCase->setAttribute( + 'time', + sprintf('%F', $time), + ); + + $this->testSuites[$this->testSuiteLevel]->appendChild( + $this->currentTestCase, + ); + + $this->testSuiteTests[$this->testSuiteLevel]++; + $this->testSuiteTimes[$this->testSuiteLevel] += $time; + + $this->currentTestCase = null; + $this->time = null; + $this->prepared = false; + } + + /** + * @throws EventFacadeIsSealedException + * @throws UnknownSubscriberTypeException + */ + private function registerSubscribers(Facade $facade): void + { + $facade->registerSubscribers( + new TestSuiteStartedSubscriber($this), + new TestSuiteFinishedSubscriber($this), + new TestPreparationStartedSubscriber($this), + new TestPreparationFailedSubscriber($this), + new TestPreparedSubscriber($this), + new TestFinishedSubscriber($this), + new TestErroredSubscriber($this), + new TestFailedSubscriber($this), + new TestMarkedIncompleteSubscriber($this), + new TestSkippedSubscriber($this), + new TestRunnerExecutionFinishedSubscriber($this), + ); + } + + private function createDocument(): void + { + $this->document = new DOMDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + + $this->root = $this->document->createElement('testsuites'); + $this->document->appendChild($this->root); + } + + /** + * @throws InvalidArgumentException + */ + private function handleFault(Errored|Failed $event, string $type): void + { + if (! $this->prepared) { + $this->createTestCase($event); + } + + assert($this->currentTestCase !== null); + + $buffer = $this->converter->getTestCaseMethodName($event->test()); // pest-changed + + $throwable = $event->throwable(); + $buffer .= trim( + $this->converter->getExceptionMessage($throwable).PHP_EOL. // pest-changed + $this->converter->getExceptionDetails($throwable), // pest-changed + ); + + $fault = $this->document->createElement( + $type, + Xml::prepareString($buffer), + ); + + $fault->setAttribute('type', $throwable->className()); + + $this->currentTestCase->appendChild($fault); + + if (! $this->prepared) { + $this->handleFinish($event->telemetryInfo(), 0); + } + } + + /** + * @throws InvalidArgumentException + */ + private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void + { + if (! $this->prepared) { + $this->createTestCase($event); + } + + assert($this->currentTestCase !== null); + + $skipped = $this->document->createElement('skipped'); + + $this->currentTestCase->appendChild($skipped); + + $this->testSuiteSkipped[$this->testSuiteLevel]++; + + if (! $this->prepared) { + $this->handleFinish($event->telemetryInfo(), 0); + } + } + + /** + * @throws InvalidArgumentException + */ + private function testAsString(Test $test): string + { + if ($test->isPhpt()) { + return basename($test->file()); + } + + assert($test instanceof TestMethod); + + return sprintf( + '%s::%s%s', + $test->className(), + $this->name($test), + PHP_EOL, + ); + } + + /** + * @throws InvalidArgumentException + */ + private function name(Test $test): string + { + if ($test->isPhpt()) { + return basename($test->file()); + } + + assert($test instanceof TestMethod); + + if (! $test->testData()->hasDataFromDataProvider()) { + return $test->methodName(); + } + + $dataSetName = $test->testData()->dataFromDataProvider()->dataSetName(); + + if (is_int($dataSetName)) { + return sprintf( + '%s with data set #%d', + $test->methodName(), + $dataSetName, + ); + } + + return sprintf( + '%s with data set "%s"', + $test->methodName(), + $dataSetName, + ); + } + + /** + * @throws InvalidArgumentException + * + * @psalm-assert !null $this->currentTestCase + */ + private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void + { + $testCase = $this->document->createElement('testcase'); + + $test = $event->test(); + $file = $this->converter->getTestCaseLocation($test); // pest-added + + $testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); // pest-changed + $testCase->setAttribute('file', $file); // pest-changed + + if ($test->isTestMethod()) { + assert($test instanceof TestMethod); + + //$testCase->setAttribute('line', (string) $test->line()); // pest-removed + $className = $this->converter->getTrimmedTestClassName($test); // pest-added + $testCase->setAttribute('class', $className); // pest-changed + $testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed + } + + $this->currentTestCase = $testCase; + $this->time = $event->telemetryInfo()->time(); } } diff --git a/overrides/TextUI/Command/WarmCodeCoverageCacheCommand.php b/overrides/TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php similarity index 100% rename from overrides/TextUI/Command/WarmCodeCoverageCacheCommand.php rename to overrides/TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php diff --git a/overrides/TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php b/overrides/TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php similarity index 100% rename from overrides/TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php rename to overrides/TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php diff --git a/resources/views/components/badge.php b/resources/views/components/badge.php index df99a599..39d31b9f 100644 --- a/resources/views/components/badge.php +++ b/resources/views/components/badge.php @@ -10,7 +10,7 @@ ?>
- + diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php index e6687a3a..bf4f53ff 100644 --- a/src/Bootstrappers/BootOverrides.php +++ b/src/Bootstrappers/BootOverrides.php @@ -15,17 +15,17 @@ final class BootOverrides implements Bootstrapper /** * The list of files to be overridden. * - * @var array + * @var array */ - private const FILES = [ - 'Runner/Filter/NameFilterIterator.php', - 'Runner/ResultCache/DefaultResultCache.php', - 'Runner/TestSuiteLoader.php', - 'TextUI/Command/WarmCodeCoverageCacheCommand.php', - 'TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php', - 'TextUI/TestSuiteFilterProcessor.php', - 'Event/Value/ThrowableBuilder.php', - 'Logging/JUnit/JunitXmlLogger.php', + public const FILES = [ + 'c7b9c8a96006dea314204a8f09a8764e51ce0b9b79aadd58da52e8c328db4870' => 'Runner/Filter/NameFilterIterator.php', + '52b2574e96269aca1bb2d41bbf418c3bcf23dd21d14c66f90789025c309e39df' => 'Runner/ResultCache/DefaultResultCache.php', + 'bc8718c89264f65800beabc23e51c6d3bcff87dfc764a12179ef5dbfde272c8b' => 'Runner/TestSuiteLoader.php', + 'f41e48d6cb546772a7de4f8e66b6b7ce894a5318d063eb52e354d206e96c701c' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', + 'cb7519f2d82893640b694492cf7ec9528da80773cc1d259634181b5d393528b5' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', + '6db25ee539e9b12b1fb4e044a0a93410e015bc983ecdd3909cd394fe44ae8c95' => 'TextUI/TestSuiteFilterProcessor.php', + 'ef64a657ed9c0067791483784944107827bf227c7e3200f212b6751876b99e25' => 'Event/Value/ThrowableBuilder.php', + 'c78f96e34b98ed01dd8106539d59b8aa8d67f733274118b827c01c5c4111c033' => 'Logging/JUnit/JunitXmlLogger.php', ]; /** diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index ad4ab7da..248b9dde 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -25,7 +25,6 @@ final class BootSubscribers implements Bootstrapper Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureKernelDumpIsFlushed::class, Subscribers\EnsureTeamCityEnabled::class, - Subscribers\EnsureJunitEnabled::class, ]; /** diff --git a/src/Concerns/Pipeable.php b/src/Concerns/Pipeable.php index 6d889f16..63ab0b7d 100644 --- a/src/Concerns/Pipeable.php +++ b/src/Concerns/Pipeable.php @@ -60,7 +60,7 @@ trait Pipeable } /** - * Get th list of pipes by the given name. + * Get the list of pipes by the given name. * * @return array */ diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 43d0a162..58c97a22 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -296,7 +296,7 @@ trait Testable return $arguments; } - if (in_array($testParameterTypes[0], [Closure::class, 'callable'])) { + if (isset($testParameterTypes[0]) && in_array($testParameterTypes[0], [Closure::class, 'callable'])) { return $arguments; } diff --git a/src/Contracts/Plugins/HandlesOriginalArguments.php b/src/Contracts/Plugins/HandlesOriginalArguments.php new file mode 100644 index 00000000..ae4e7a54 --- /dev/null +++ b/src/Contracts/Plugins/HandlesOriginalArguments.php @@ -0,0 +1,18 @@ + $arguments + */ + public function handleOriginalArguments(array $arguments): void; +} diff --git a/src/Contracts/Plugins/Shutdownable.php b/src/Contracts/Plugins/Terminable.php similarity index 54% rename from src/Contracts/Plugins/Shutdownable.php rename to src/Contracts/Plugins/Terminable.php index 41b91fa7..a53a903f 100644 --- a/src/Contracts/Plugins/Shutdownable.php +++ b/src/Contracts/Plugins/Terminable.php @@ -7,10 +7,10 @@ namespace Pest\Contracts\Plugins; /** * @internal */ -interface Shutdownable +interface Terminable { /** - * Shutdowns the plugin. + * Terminates the plugin. */ - public function shutdown(): void; + public function terminate(): void; } diff --git a/src/Exceptions/FatalException.php b/src/Exceptions/FatalException.php new file mode 100644 index 00000000..d5f9d31f --- /dev/null +++ b/src/Exceptions/FatalException.php @@ -0,0 +1,16 @@ +value->$method(...), $parameters)); } - ExpectationPipeline::for($this->getExpectationClosure($method)) + $closure = $this->getExpectationClosure($method); + $reflectionClosure = new \ReflectionFunction($closure); + $expectation = $reflectionClosure->getClosureThis(); + + assert(is_object($expectation)); + + ExpectationPipeline::for($closure) ->send(...$parameters) - ->through($this->pipes($method, $this, Expectation::class)) + ->through($this->pipes($method, $expectation, Expectation::class)) ->run(); return $this; @@ -876,4 +883,51 @@ final class Expectation { return $this->toHaveMethod('__destruct'); } + + /** + * Asserts that the given expectation target is a backed enum of given type. + */ + private function toBeBackedEnum(string $backingType): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum() + && (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line + && (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line + 'to be '.$backingType.' backed enum', + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), + ); + } + + /** + * Asserts that the given expectation targets are string backed enums. + */ + public function toBeStringBackedEnums(): ArchExpectation + { + return $this->toBeStringBackedEnum(); + } + + /** + * Asserts that the given expectation targets are int backed enums. + */ + public function toBeIntBackedEnums(): ArchExpectation + { + return $this->toBeIntBackedEnum(); + } + + /** + * Asserts that the given expectation target is a string backed enum. + */ + public function toBeStringBackedEnum(): ArchExpectation + { + return $this->toBeBackedEnum('string'); + } + + /** + * Asserts that the given expectation target is an int backed enum. + */ + public function toBeIntBackedEnum(): ArchExpectation + { + return $this->toBeBackedEnum('int'); + } } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 66963677..57796f83 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -485,4 +485,51 @@ final class OppositeExpectation { return $this->toHaveMethod('__destruct'); } + + /** + * Asserts that the given expectation target is not a backed enum of given type. + */ + private function toBeBackedEnum(string $backingType): ArchExpectation + { + return Targeted::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum() + || ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line + || (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line + 'not to be '.$backingType.' backed enum', + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), + ); + } + + /** + * Asserts that the given expectation targets are not string backed enums. + */ + public function toBeStringBackedEnums(): ArchExpectation + { + return $this->toBeStringBackedEnum(); + } + + /** + * Asserts that the given expectation targets are not int backed enums. + */ + public function toBeIntBackedEnums(): ArchExpectation + { + return $this->toBeIntBackedEnum(); + } + + /** + * Asserts that the given expectation target is not a string backed enum. + */ + public function toBeStringBackedEnum(): ArchExpectation + { + return $this->toBeBackedEnum('string'); + } + + /** + * Asserts that the given expectation target is not an int backed enum. + */ + public function toBeIntBackedEnum(): ArchExpectation + { + return $this->toBeBackedEnum('int'); + } } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index e00da683..9255cf9b 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -74,7 +74,7 @@ final class TestCaseMethodFactory public ?Closure $closure, ) { $this->closure ??= function (): void { - Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line + (Assert::getCount() > 0 || $this->doesNotPerformAssertions()) ?: self::markTestIncomplete(); // @phpstan-ignore-line }; $this->bootHigherOrderable(); diff --git a/src/Kernel.php b/src/Kernel.php index c5267a79..b8683a20 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -4,18 +4,25 @@ declare(strict_types=1); namespace Pest; +use NunoMaduro\Collision\Writer; use Pest\Contracts\Bootstrapper; +use Pest\Exceptions\FatalException; use Pest\Exceptions\NoDirtyTestsFound; use Pest\Plugins\Actions\CallsAddsOutput; use Pest\Plugins\Actions\CallsBoot; use Pest\Plugins\Actions\CallsHandleArguments; -use Pest\Plugins\Actions\CallsShutdown; +use Pest\Plugins\Actions\CallsHandleOriginalArguments; +use Pest\Plugins\Actions\CallsTerminable; use Pest\Support\Container; +use Pest\Support\Reflection; +use Pest\Support\View; 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; +use Throwable; +use Whoops\Exception\Inspector; /** * @internal @@ -43,7 +50,7 @@ final class Kernel private readonly Application $application, private readonly OutputInterface $output, ) { - register_shutdown_function(fn () => $this->shutdown()); + // } /** @@ -59,6 +66,13 @@ final class Kernel ->add(OutputInterface::class, $output) ->add(Container::class, $container); + $kernel = new self( + new Application(), + $output, + ); + + register_shutdown_function(fn () => $kernel->shutdown()); + foreach (self::BOOTSTRAPPERS as $bootstrapper) { $bootstrapper = Container::getInstance()->get($bootstrapper); assert($bootstrapper instanceof Bootstrapper); @@ -68,11 +82,6 @@ final class Kernel CallsBoot::execute(); - $kernel = new self( - new Application(), - $output, - ); - Container::getInstance()->add(self::class, $kernel); return $kernel; @@ -81,14 +90,17 @@ final class Kernel /** * Runs the application, and returns the exit code. * - * @param array $args + * @param array $originalArguments + * @param array $arguments */ - public function handle(array $args): int + public function handle(array $originalArguments, array $arguments): int { - $args = CallsHandleArguments::execute($args); + CallsHandleOriginalArguments::execute($originalArguments); + + $arguments = CallsHandleArguments::execute($arguments); try { - $this->application->run($args); + $this->application->run($arguments); } catch (NoDirtyTestsFound) { $this->output->writeln([ '', @@ -106,16 +118,54 @@ final class Kernel } /** - * Shutdown the Kernel. + * Terminate the Kernel. */ - public function shutdown(): void + public function terminate(): void { $preBufferOutput = Container::getInstance()->get(KernelDump::class); assert($preBufferOutput instanceof KernelDump); - $preBufferOutput->shutdown(); + $preBufferOutput->terminate(); - CallsShutdown::execute(); + CallsTerminable::execute(); + } + + /** + * Shutdowns unexpectedly the Kernel. + */ + public function shutdown(): void + { + $this->terminate(); + + if (is_array($error = error_get_last())) { + if (! in_array($error['type'], [E_ERROR, E_CORE_ERROR], true)) { + return; + } + + $message = $error['message']; + $file = $error['file']; + $line = $error['line']; + + try { + $writer = new Writer(null, $this->output); + + $throwable = new FatalException($message); + + Reflection::setPropertyValue($throwable, 'line', $line); + Reflection::setPropertyValue($throwable, 'file', $file); + + $inspector = new Inspector($throwable); + + $writer->write($inspector); + } catch (Throwable) { // @phpstan-ignore-line + View::render('components.badge', [ + 'type' => 'ERROR', + 'content' => sprintf('%s in %s:%d', $message, $file, $line), + ]); + } + + exit(1); + } } } diff --git a/src/KernelDump.php b/src/KernelDump.php index 39f2004b..150e44ae 100644 --- a/src/KernelDump.php +++ b/src/KernelDump.php @@ -48,9 +48,9 @@ final class KernelDump } /** - * Shutdown the output buffering. + * Terminate the output buffering. */ - public function shutdown(): void + public function terminate(): void { $this->disable(); } diff --git a/src/Logging/JUnit/JUnitLogger.php b/src/Logging/JUnit/JUnitLogger.php deleted file mode 100644 index ba71f88f..00000000 --- a/src/Logging/JUnit/JUnitLogger.php +++ /dev/null @@ -1,384 +0,0 @@ - - */ - private array $testSuiteTests = [0]; - - /** - * @psalm-var array - */ - private array $testSuiteAssertions = [0]; - - /** - * @psalm-var array - */ - private array $testSuiteErrors = [0]; - - /** - * @psalm-var array - */ - private array $testSuiteFailures = [0]; - - /** - * @psalm-var array - */ - private array $testSuiteSkipped = [0]; - - /** - * @psalm-var array - */ - private array $testSuiteTimes = [0]; - - private int $testSuiteLevel = 0; - - private ?DOMElement $currentTestCase = null; - - private ?HRTime $time = null; - - private bool $prepared = false; - - /** - * @throws EventFacadeIsSealedException - * @throws UnknownSubscriberTypeException - */ - public function __construct( - private readonly Printer $printer, - private readonly Converter $converter, - ) { - $this->registerSubscribers(); - $this->createDocument(); - } - - public function flush(): void - { - $this->printer->print((string) $this->document->saveXML()); - - $this->printer->flush(); - } - - public function testSuiteStarted(Started $event): void - { - $testSuite = $this->document->createElement('testsuite'); - $testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); - - if ($event->testSuite()->isForTestClass()) { - $testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); - } - - if ($this->testSuiteLevel > 0) { - $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); - } else { - $this->root->appendChild($testSuite); - } - - $this->testSuiteLevel++; - $this->testSuites[$this->testSuiteLevel] = $testSuite; - $this->testSuiteTests[$this->testSuiteLevel] = 0; - $this->testSuiteAssertions[$this->testSuiteLevel] = 0; - $this->testSuiteErrors[$this->testSuiteLevel] = 0; - $this->testSuiteFailures[$this->testSuiteLevel] = 0; - $this->testSuiteSkipped[$this->testSuiteLevel] = 0; - $this->testSuiteTimes[$this->testSuiteLevel] = 0; - } - - public function testSuiteFinished(): void - { - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'tests', - (string) $this->testSuiteTests[$this->testSuiteLevel], - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'assertions', - (string) $this->testSuiteAssertions[$this->testSuiteLevel], - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'errors', - (string) $this->testSuiteErrors[$this->testSuiteLevel], - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'failures', - (string) $this->testSuiteFailures[$this->testSuiteLevel], - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'skipped', - (string) $this->testSuiteSkipped[$this->testSuiteLevel], - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'time', - sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]), - ); - - if ($this->testSuiteLevel > 1) { - $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; - $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; - $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; - $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; - $this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel]; - $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; - } - - $this->testSuiteLevel--; - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - */ - public function testPrepared(Prepared $event): void - { - $this->createTestCase($event); - $this->prepared = true; - } - - /** - * @throws InvalidArgumentException - */ - public function testFinished(Finished $event): void - { - $this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed()); - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - */ - public function testMarkedIncomplete(MarkedIncomplete $event): void - { - $this->handleIncompleteOrSkipped($event); - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - */ - public function testSkipped(Skipped $event): void - { - $this->handleIncompleteOrSkipped($event); - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - */ - public function testErrored(Errored $event): void - { - $this->handleFault($event, 'error'); - - $this->testSuiteErrors[$this->testSuiteLevel]++; - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - */ - public function testFailed(Failed $event): void - { - $this->handleFault($event, 'failure'); - - $this->testSuiteFailures[$this->testSuiteLevel]++; - } - - /** - * @throws InvalidArgumentException - */ - private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void - { - assert($this->currentTestCase instanceof \DOMElement); - assert($this->time instanceof \PHPUnit\Event\Telemetry\HRTime); - - $time = $telemetryInfo->time()->duration($this->time)->asFloat(); - - $this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed; - - $this->currentTestCase->setAttribute( - 'assertions', - (string) $numberOfAssertionsPerformed, - ); - - $this->currentTestCase->setAttribute( - 'time', - sprintf('%F', $time), - ); - - $this->testSuites[$this->testSuiteLevel]->appendChild( - $this->currentTestCase, - ); - - $this->testSuiteTests[$this->testSuiteLevel]++; - $this->testSuiteTimes[$this->testSuiteLevel] += $time; - - $this->currentTestCase = null; - $this->time = null; - $this->prepared = false; - } - - /** - * @throws EventFacadeIsSealedException - * @throws UnknownSubscriberTypeException - */ - private function registerSubscribers(): void - { - $subscribers = [ - new TestSuiteStartedSubscriber($this), - new TestSuiteFinishedSubscriber($this), - new TestPreparedSubscriber($this), - new TestFinishedSubscriber($this), - new TestErroredSubscriber($this), - new TestFailedSubscriber($this), - new TestMarkedIncompleteSubscriber($this), - new TestSkippedSubscriber($this), - new TestRunnerExecutionFinishedSubscriber($this), - ]; - - Facade::instance()->registerSubscribers(...$subscribers); - } - - private function createDocument(): void - { - $this->document = new DOMDocument('1.0', 'UTF-8'); - $this->document->formatOutput = true; - - $this->root = $this->document->createElement('testsuites'); - $this->document->appendChild($this->root); - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - */ - private function handleFault(Errored|Failed $event, string $type): void - { - if (! $this->prepared) { - $this->createTestCase($event); - } - - assert($this->currentTestCase instanceof \DOMElement); - - $throwable = $event->throwable(); - - $testName = $this->converter->getTestCaseMethodName($event->test()); - $message = $this->converter->getExceptionMessage($throwable); - $details = $this->converter->getExceptionDetails($throwable); - - $buffer = $testName; - $buffer .= trim( - $message.PHP_EOL. - $details, - ); - - $fault = $this->document->createElement( - $type, - Xml::prepareString($buffer), - ); - - $fault->setAttribute('type', $throwable->className()); - - $this->currentTestCase->appendChild($fault); - - if (! $this->prepared) { - $this->handleFinish($event->telemetryInfo(), 0); - } - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - */ - private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void - { - if (! $this->prepared) { - $this->createTestCase($event); - } - - assert($this->currentTestCase instanceof \DOMElement); - - $skipped = $this->document->createElement('skipped'); - - $this->currentTestCase->appendChild($skipped); - - $this->testSuiteSkipped[$this->testSuiteLevel]++; - - if (! $this->prepared) { - $this->handleFinish($event->telemetryInfo(), 0); - } - } - - /** - * @throws InvalidArgumentException - * @throws NoDataSetFromDataProviderException - * - * @psalm-assert !null $this->currentTestCase - */ - private function createTestCase(Errored|Failed|MarkedIncomplete|Prepared|Skipped $event): void - { - $testCase = $this->document->createElement('testcase'); - - $test = $event->test(); - $file = $this->converter->getTestCaseLocation($test); - - $testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); - $testCase->setAttribute('file', $file); - - if ($test->isTestMethod()) { - $className = $this->converter->getTrimmedTestClassName($test); - $testCase->setAttribute('class', $className); - $testCase->setAttribute('classname', str_replace('\\', '.', $className)); - } - - $this->currentTestCase = $testCase; - $this->time = $event->telemetryInfo()->time(); - } -} diff --git a/src/Logging/JUnit/Subscriber/Subscriber.php b/src/Logging/JUnit/Subscriber/Subscriber.php deleted file mode 100644 index 7ffa1f34..00000000 --- a/src/Logging/JUnit/Subscriber/Subscriber.php +++ /dev/null @@ -1,28 +0,0 @@ -logger; - } -} diff --git a/src/Logging/JUnit/Subscriber/TestErroredSubscriber.php b/src/Logging/JUnit/Subscriber/TestErroredSubscriber.php deleted file mode 100644 index 87172774..00000000 --- a/src/Logging/JUnit/Subscriber/TestErroredSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testErrored($event); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestFailedSubscriber.php b/src/Logging/JUnit/Subscriber/TestFailedSubscriber.php deleted file mode 100644 index d0f6c1ea..00000000 --- a/src/Logging/JUnit/Subscriber/TestFailedSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testFailed($event); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestFinishedSubscriber.php b/src/Logging/JUnit/Subscriber/TestFinishedSubscriber.php deleted file mode 100644 index 4c1e937d..00000000 --- a/src/Logging/JUnit/Subscriber/TestFinishedSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testFinished($event); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestMarkedIncompleteSubscriber.php b/src/Logging/JUnit/Subscriber/TestMarkedIncompleteSubscriber.php deleted file mode 100644 index 4ed5d693..00000000 --- a/src/Logging/JUnit/Subscriber/TestMarkedIncompleteSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testMarkedIncomplete($event); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestPreparedSubscriber.php b/src/Logging/JUnit/Subscriber/TestPreparedSubscriber.php deleted file mode 100644 index f9841361..00000000 --- a/src/Logging/JUnit/Subscriber/TestPreparedSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testPrepared($event); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestRunnerExecutionFinishedSubscriber.php b/src/Logging/JUnit/Subscriber/TestRunnerExecutionFinishedSubscriber.php deleted file mode 100644 index 8dcf762f..00000000 --- a/src/Logging/JUnit/Subscriber/TestRunnerExecutionFinishedSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->flush(); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestSkippedSubscriber.php b/src/Logging/JUnit/Subscriber/TestSkippedSubscriber.php deleted file mode 100644 index afa764ad..00000000 --- a/src/Logging/JUnit/Subscriber/TestSkippedSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testSkipped($event); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestSuiteFinishedSubscriber.php b/src/Logging/JUnit/Subscriber/TestSuiteFinishedSubscriber.php deleted file mode 100644 index 9ed15c15..00000000 --- a/src/Logging/JUnit/Subscriber/TestSuiteFinishedSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testSuiteFinished(); - } -} diff --git a/src/Logging/JUnit/Subscriber/TestSuiteStartedSubscriber.php b/src/Logging/JUnit/Subscriber/TestSuiteStartedSubscriber.php deleted file mode 100644 index 26f80239..00000000 --- a/src/Logging/JUnit/Subscriber/TestSuiteStartedSubscriber.php +++ /dev/null @@ -1,19 +0,0 @@ -logger()->testSuiteStarted($event); - } -} diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 0d0b4d92..71117657 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -131,7 +131,7 @@ final class Expectation * * @return self */ - public function toBeGreaterThan(int|float|DateTimeInterface $expected, string $message = ''): self + public function toBeGreaterThan(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertGreaterThan($expected, $this->value, $message); @@ -143,7 +143,7 @@ final class Expectation * * @return self */ - public function toBeGreaterThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self + public function toBeGreaterThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertGreaterThanOrEqual($expected, $this->value, $message); @@ -155,7 +155,7 @@ final class Expectation * * @return self */ - public function toBeLessThan(int|float|DateTimeInterface $expected, string $message = ''): self + public function toBeLessThan(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertLessThan($expected, $this->value, $message); @@ -167,7 +167,7 @@ final class Expectation * * @return self */ - public function toBeLessThanOrEqual(int|float|DateTimeInterface $expected, string $message = ''): self + public function toBeLessThanOrEqual(int|float|string|DateTimeInterface $expected, string $message = ''): self { Assert::assertLessThanOrEqual($expected, $this->value, $message); @@ -196,6 +196,24 @@ final class Expectation return $this; } + /** + * Asserts that $needle equal an element of the value. + * + * @return self + */ + public function toContainEqual(mixed ...$needles): self + { + if (! is_iterable($this->value)) { + InvalidExpectationValue::expected('iterable'); + } + + foreach ($needles as $needle) { + Assert::assertContainsEquals($needle, $this->value); + } + + return $this; + } + /** * Asserts that the value starts with $expected. * diff --git a/src/Pest.php b/src/Pest.php index f7d99f41..ac2e5b6a 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '3.0.0-dev-0004'; + return '3.0.0-dev-0005'; } function testDirectory(string $file = ''): string diff --git a/src/Plugins/Actions/CallsHandleOriginalArguments.php b/src/Plugins/Actions/CallsHandleOriginalArguments.php new file mode 100644 index 00000000..5917df7b --- /dev/null +++ b/src/Plugins/Actions/CallsHandleOriginalArguments.php @@ -0,0 +1,31 @@ + $argv + */ + public static function execute(array $argv): void + { + $plugins = Loader::getPlugins(Plugins\HandlesOriginalArguments::class); + + /** @var Plugins\HandlesOriginalArguments $plugin */ + foreach ($plugins as $plugin) { + $plugin->handleOriginalArguments($argv); + } + } +} diff --git a/src/Plugins/Actions/CallsShutdown.php b/src/Plugins/Actions/CallsTerminable.php similarity index 56% rename from src/Plugins/Actions/CallsShutdown.php rename to src/Plugins/Actions/CallsTerminable.php index d694e2b8..619b9c0f 100644 --- a/src/Plugins/Actions/CallsShutdown.php +++ b/src/Plugins/Actions/CallsTerminable.php @@ -10,20 +10,20 @@ use Pest\Plugin\Loader; /** * @internal */ -final class CallsShutdown +final class CallsTerminable { /** * Executes the Plugin action. * - * Provides an opportunity for any plugins to shutdown. + * Provides an opportunity for any plugins to terminate. */ public static function execute(): void { - $plugins = Loader::getPlugins(Plugins\Shutdownable::class); + $plugins = Loader::getPlugins(Plugins\Terminable::class); - /** @var Plugins\Shutdownable $plugin */ + /** @var Plugins\Terminable $plugin */ foreach ($plugins as $plugin) { - $plugin->shutdown(); + $plugin->terminate(); } } } diff --git a/src/Plugins/Cache.php b/src/Plugins/Cache.php index 7312691b..ea3abb78 100644 --- a/src/Plugins/Cache.php +++ b/src/Plugins/Cache.php @@ -6,6 +6,10 @@ namespace Pest\Plugins; use Pest\Contracts\Plugins\HandlesArguments; use Pest\Plugins\Concerns\HandleArguments; +use PHPUnit\TextUI\CliArguments\Builder as CliConfigurationBuilder; +use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder; +use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration; +use PHPUnit\TextUI\XmlConfiguration\Loader; /** * @internal @@ -31,9 +35,19 @@ final class Cache implements HandlesArguments public function handleArguments(array $arguments): array { if (! $this->hasArgument('--cache-directory', $arguments)) { - $arguments = $this->pushArgument('--cache-directory', $arguments); - $arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments); + $cliConfiguration = (new CliConfigurationBuilder)->fromParameters([]); + $configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration); + $xmlConfiguration = DefaultConfiguration::create(); + + if (is_string($configurationFile)) { + $xmlConfiguration = (new Loader)->load($configurationFile); + } + + if (! $xmlConfiguration->phpunit()->hasCacheDirectory()) { + $arguments = $this->pushArgument('--cache-directory', $arguments); + $arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments); + } } if (! $this->hasArgument('--parallel', $arguments)) { diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index 668ff09f..41f28f5b 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -128,9 +128,9 @@ final class Coverage implements AddsOutput, HandlesArguments if ($exitCode === 1) { $this->output->writeln(sprintf( - "\n FAIL Code coverage below expected: %s %%. Minimum: %s %%.", - number_format($coverage, 1), - number_format($this->coverageMin, 1) + "\n FAIL Code coverage below expected %s %%, currently %s %%.", + number_format($this->coverageMin, 1), + number_format($coverage, 1) )); } diff --git a/src/Plugins/Help.php b/src/Plugins/Help.php index 9e1c3f53..205610f0 100644 --- a/src/Plugins/Help.php +++ b/src/Plugins/Help.php @@ -61,6 +61,10 @@ final class Help implements HandlesArguments assert(is_string($argument)); + if (trim($argument) === '--process-isolation') { + continue; + } + View::render('components.two-column-detail', [ 'left' => $this->colorizeOptions($argument), 'right' => preg_replace(['//'], ['[', ']'], $description), diff --git a/src/Plugins/JUnit.php b/src/Plugins/JUnit.php deleted file mode 100644 index 3ee9bdf7..00000000 --- a/src/Plugins/JUnit.php +++ /dev/null @@ -1,44 +0,0 @@ -hasArgument('--log-junit', $arguments)) { - return $arguments; - } - - $logUnitArgument = null; - - $arguments = array_filter($arguments, function (string $argument) use (&$logUnitArgument): bool { - if (str_starts_with($argument, '--log-junit')) { - $logUnitArgument = $argument; - - return false; - } - - return true; - }); - - assert(is_string($logUnitArgument)); - - $arguments[] = $logUnitArgument; - - return array_values($arguments); - } -} diff --git a/src/Plugins/Only.php b/src/Plugins/Only.php index aa4556b8..3fe161af 100644 --- a/src/Plugins/Only.php +++ b/src/Plugins/Only.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace Pest\Plugins; -use Pest\Contracts\Plugins\Shutdownable; +use Pest\Contracts\Plugins\Terminable; use Pest\PendingCalls\TestCall; /** * @internal */ -final class Only implements Shutdownable +final class Only implements Terminable { /** * The temporary folder. @@ -26,7 +26,7 @@ final class Only implements Shutdownable /** * {@inheritDoc} */ - public function shutdown(): void + public function terminate(): void { $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; @@ -40,6 +40,10 @@ final class Only implements Shutdownable */ public static function enable(TestCall $testCall): void { + if (Environment::name() == Environment::CI) { + return; + } + $testCall->group('__pest_only'); $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 46c1f994..b0dc5232 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -363,6 +363,15 @@ final class WrapperRunner implements RunnerInterface $this->codeCoverageFilterRegistry, false, ); + if (! $coverageManager->isActive()) { + $this->output->writeln([ + '', + ' WARN No code coverage driver is available.', + '', + ]); + + return; + } $coverageMerger = new CoverageMerger($coverageManager->codeCoverage()); foreach ($this->coverageFiles as $coverageFile) { $coverageMerger->addCoverageFromFile($coverageFile); diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 0202cb4b..b8896f77 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -27,7 +27,7 @@ final class TestRepository private array $testCases = []; /** - * @var array, 1: array, 2: array}> + * @var array, 1: array, 2: array>}> */ private array $uses = []; @@ -79,12 +79,17 @@ final class TestRepository throw new TestCaseClassOrTraitNotFound($classOrTrait); } + $hooks = array_map(fn (Closure $hook): array => [$hook], $hooks); + foreach ($paths as $path) { if (array_key_exists($path, $this->uses)) { $this->uses[$path] = [ [...$this->uses[$path][0], ...$classOrTraits], [...$this->uses[$path][1], ...$groups], - $this->uses[$path][2] + $hooks, + array_map( + fn (int $index): array => [...$this->uses[$path][2][$index] ?? [], ...($hooks[$index] ?? [])], + range(0, 3), + ), ]; } else { $this->uses[$path] = [$classOrTraits, $groups, $hooks]; @@ -190,10 +195,15 @@ final class TestRepository } } - $testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeAll', [$hooks[0] ?? null]); - $testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeEach', [$hooks[1] ?? null]); - $testCase->factoryProxies->add($testCase->filename, 0, '__addAfterEach', [$hooks[2] ?? null]); - $testCase->factoryProxies->add($testCase->filename, 0, '__addAfterAll', [$hooks[3] ?? null]); + foreach ($testCase->methods as $method) { + $method->groups = [...$groups, ...$method->groups]; + } + + foreach (['__addBeforeAll', '__addBeforeEach', '__addAfterEach', '__addAfterAll'] as $index => $name) { + foreach ($hooks[$index] ?? [null] as $hook) { + $testCase->factoryProxies->add($testCase->filename, 0, $name, [$hook]); + } + } } } diff --git a/src/Subscribers/EnsureJunitEnabled.php b/src/Subscribers/EnsureJunitEnabled.php deleted file mode 100644 index b57c32e6..00000000 --- a/src/Subscribers/EnsureJunitEnabled.php +++ /dev/null @@ -1,48 +0,0 @@ -input->hasParameterOption('--log-junit')) { - return; - } - - $configuration = Container::getInstance()->get(Configuration::class); - assert($configuration instanceof Configuration); - - new JUnitLogger( - DefaultPrinter::from($configuration->logfileJunit()), - new Converter($this->testSuite->rootPath), - ); - } -} diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php index 6dc38828..03001976 100644 --- a/src/Support/Backtrace.php +++ b/src/Support/Backtrace.php @@ -115,7 +115,11 @@ final class Backtrace continue; } - if (str_contains($trace['file'], 'pest'.DIRECTORY_SEPARATOR.'src')) { + if (($GLOBALS['__PEST_INTERNAL_TEST_SUITE'] ?? false) && str_contains($trace['file'], 'pest'.DIRECTORY_SEPARATOR.'src')) { + continue; + } + + if (str_contains($trace['file'], DIRECTORY_SEPARATOR.'pestphp'.DIRECTORY_SEPARATOR.'pest'.DIRECTORY_SEPARATOR.'src')) { continue; } diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index 395a517e..68581b85 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Pest\Support; use Closure; +use InvalidArgumentException; use Pest\Exceptions\ShouldNotHappen; use Pest\TestSuite; use ReflectionClass; @@ -66,9 +67,17 @@ final class Reflection { $test = TestSuite::getInstance()->test; - return $test instanceof \PHPUnit\Framework\TestCase - ? Closure::fromCallable($callable)->bindTo($test)(...$test->providedData()) - : self::bindCallable($callable); + if (! $test instanceof \PHPUnit\Framework\TestCase) { + return self::bindCallable($callable); + } + + foreach ($test->providedData() as $value) { + if ($value instanceof Closure) { + throw new InvalidArgumentException('Bound datasets are not supported while doing high order testing.'); + } + } + + return Closure::fromCallable($callable)->bindTo($test)(...$test->providedData()); } /** diff --git a/tests/.cache/test-results b/tests/.cache/test-results new file mode 100644 index 00000000..9c485c33 --- /dev/null +++ b/tests/.cache/test-results @@ -0,0 +1 @@ +{"version":"pest_2.32.2","defects":[],"times":{"P\\Tests\\Playground::__pest_evaluable_basic":0.005}} \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_using_pipes.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_using_pipes.snap new file mode 100644 index 00000000..a7233b4e --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_using_pipes.snap @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index b2e55fe2..ed1242e4 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -1,5 +1,5 @@ - Pest Testing Framework 3.0.0-dev-0004. + Pest Testing Framework 3.0.0-dev-0005. USAGE: pest [options] @@ -40,7 +40,6 @@ EXECUTION OPTIONS: --parallel ........................................... Run tests in parallel --update-snapshots Update snapshots for tests using the "toMatchSnapshot" expectation - --process-isolation ................ Run each test in a separate PHP process --globals-backup ................. Backup and restore $GLOBALS for each test --static-backup ......... Backup and restore static properties for each test --strict-coverage ................... Be strict about code coverage metadata diff --git a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap index 8174bcde..3eba2c19 100644 --- a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap @@ -1,3 +1,3 @@ - Pest Testing Framework 3.0.0-dev-0004. + Pest Testing Framework 3.0.0-dev-0005. diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 62d136c2..a7e7dcb6 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -151,6 +151,10 @@ ✓ it can correctly resolve a bound dataset that returns an array with (Closure) ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure) ↓ forbids to define tests in Datasets dirs and Datasets.php files + ✓ it may be used with high order with dataset "formal" + ✓ it may be used with high order with dataset "informal" + ✓ it may be used with high order even when bound with dataset "formal" + ✓ it may be used with high order even when bound with dataset "informal" PASS Tests\Features\Depends ✓ first @@ -423,6 +427,7 @@ PASS Tests\Features\Expect\toBeGreaterThan ✓ passes ✓ passes with DateTime and DateTimeImmutable + ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures @@ -430,6 +435,7 @@ PASS Tests\Features\Expect\toBeGreaterThanOrEqual ✓ passes ✓ passes with DateTime and DateTimeImmutable + ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures @@ -458,6 +464,10 @@ ✓ failures with custom message ✓ not failures + PASS Tests\Features\Expect\toBeIntBackedEnum + ✓ enum is backed by int + ✓ enum is not backed by int + PASS Tests\Features\Expect\toBeInvokable ✓ class is invokable ✓ opposite class is invokable @@ -487,6 +497,7 @@ PASS Tests\Features\Expect\toBeLessThan ✓ passes ✓ passes with DateTime and DateTimeImmutable + ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures @@ -494,6 +505,7 @@ PASS Tests\Features\Expect\toBeLessThanOrEqual ✓ passes ✓ passes with DateTime and DateTimeImmutable + ✓ passes with strings ✓ failures ✓ failures with custom message ✓ not failures @@ -564,6 +576,10 @@ ✓ failures with custom message ✓ not failures + PASS Tests\Features\Expect\toBeStringBackedEnum + ✓ enum is backed by string + ✓ enum is not backed by string + PASS Tests\Features\Expect\toBeStudlyCase ✓ pass ✓ failures @@ -634,6 +650,16 @@ ✓ failures with multiple needles (some failing) ✓ not failures ✓ not failures with multiple needles (all failing) + ✓ not failures with multiple needles (some failing) + + PASS Tests\Features\Expect\toContainEqual + ✓ passes arrays + ✓ passes arrays with multiple needles + ✓ failures + ✓ failures with multiple needles (all failing) + ✓ failures with multiple needles (some failing) + ✓ not failures + ✓ not failures with multiple needles (all failing) ✓ not failures with multiple needles (some failing) PASS Tests\Features\Expect\toContainOnlyInstancesOf @@ -827,6 +853,7 @@ PASS Tests\Features\Expect\toMatchSnapshot ✓ pass + ✓ pass using pipes ✓ pass with __toString ✓ pass with toString ✓ pass with dataset with ('my-datas-set-value') @@ -1137,6 +1164,16 @@ PASS Tests\Hooks\BeforeEachTest ✓ global beforeEach execution order + PASS Tests\Overrides\VersionsTest + ✓ versions with dataset "Runner/Filter/NameFilterIterator.php" + ✓ versions with dataset "Runner/ResultCache/DefaultResultCache.php" + ✓ versions with dataset "Runner/TestSuiteLoader.php" + ✓ versions with dataset "TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php" + ✓ versions with dataset "TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php" + ✓ versions with dataset "TextUI/TestSuiteFilterProcessor.php" + ✓ versions with dataset "Event/Value/ThrowableBuilder.php" + ✓ versions with dataset "Logging/JUnit/JunitXmlLogger.php" + PASS Tests\PHPUnit\CustomAffixes\InvalidTestName ✓ it runs file names like @#$%^&()-_=+.php @@ -1348,9 +1385,13 @@ PASS Tests\Visual\Help ✓ visual snapshot of help command output - WARN Tests\Visual\Parallel - - parallel → Waiting for Parallel to be stable - - a parallel test can extend another test with same name → Waiting for Parallel to be stable + PASS Tests\Visual\JUnit + ✓ junit output + ✓ junit with parallel + + PASS Tests\Visual\Parallel + ✓ parallel + ✓ a parallel test can extend another test with same name PASS Tests\Visual\SingleTestOrDirectory ✓ allows to run a single test @@ -1373,4 +1414,4 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 24 skipped, 970 passed (2295 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 24 skipped, 970 passed (2295 assertions) diff --git a/tests/Features/DatasetsTests.php b/tests/Features/DatasetsTests.php index f6030926..7d209c40 100644 --- a/tests/Features/DatasetsTests.php +++ b/tests/Features/DatasetsTests.php @@ -361,3 +361,23 @@ it('can correctly resolve a bound dataset that returns an array but wants to be ]); todo('forbids to define tests in Datasets dirs and Datasets.php files'); + +dataset('greeting-string', [ + 'formal' => 'Evening', + 'informal' => 'yo', +]); + +it('may be used with high order') + ->with('greeting-string') + ->expect(fn (string $greeting) => $greeting) + ->throwsNoExceptions(); + +dataset('greeting-bound', [ + 'formal' => fn () => 'Evening', + 'informal' => fn () => 'yo', +]); + +it('may be used with high order even when bound') + ->with('greeting-bound') + ->expect(fn (string $greeting) => $greeting) + ->throws(InvalidArgumentException::class); diff --git a/tests/Features/Expect/toBeGreaterThan.php b/tests/Features/Expect/toBeGreaterThan.php index a3ceafef..f59c6628 100644 --- a/tests/Features/Expect/toBeGreaterThan.php +++ b/tests/Features/Expect/toBeGreaterThan.php @@ -16,6 +16,11 @@ test('passes with DateTime and DateTimeImmutable', function () { expect($past)->not->toBeGreaterThan($now); }); +test('passes with strings', function () { + expect('b')->toBeGreaterThan('a'); + expect('a')->not->toBeGreaterThan('a'); +}); + test('failures', function () { expect(4)->toBeGreaterThan(4); })->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeGreaterThanOrEqual.php b/tests/Features/Expect/toBeGreaterThanOrEqual.php index 4a0f62ad..52aa3b18 100644 --- a/tests/Features/Expect/toBeGreaterThanOrEqual.php +++ b/tests/Features/Expect/toBeGreaterThanOrEqual.php @@ -18,6 +18,11 @@ test('passes with DateTime and DateTimeImmutable', function () { expect($past)->not->toBeGreaterThanOrEqual($now); }); +test('passes with strings', function () { + expect('b')->toBeGreaterThanOrEqual('a'); + expect('a')->toBeGreaterThanOrEqual('a'); +}); + test('failures', function () { expect(4)->toBeGreaterThanOrEqual(4.1); })->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeIntBackedEnum.php b/tests/Features/Expect/toBeIntBackedEnum.php new file mode 100644 index 00000000..10ee86c7 --- /dev/null +++ b/tests/Features/Expect/toBeIntBackedEnum.php @@ -0,0 +1,9 @@ +expect('Tests\Fixtures\Arch\ToBeIntBackedEnum\HasIntBacking') + ->toBeIntBackedEnum(); + +test('enum is not backed by int') + ->expect('Tests\Fixtures\Arch\ToBeIntBackedEnum\HasStringBacking') + ->not->toBeIntBackedEnum(); diff --git a/tests/Features/Expect/toBeLessThan.php b/tests/Features/Expect/toBeLessThan.php index 802c1c08..f7de96fa 100644 --- a/tests/Features/Expect/toBeLessThan.php +++ b/tests/Features/Expect/toBeLessThan.php @@ -16,6 +16,11 @@ test('passes with DateTime and DateTimeImmutable', function () { expect($now)->not->toBeLessThan($now); }); +test('passes with strings', function () { + expect('a')->toBeLessThan('b'); + expect('a')->not->toBeLessThan('a'); +}); + test('failures', function () { expect(4)->toBeLessThan(4); })->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeLessThanOrEqual.php b/tests/Features/Expect/toBeLessThanOrEqual.php index e5643759..f25f0774 100644 --- a/tests/Features/Expect/toBeLessThanOrEqual.php +++ b/tests/Features/Expect/toBeLessThanOrEqual.php @@ -18,6 +18,11 @@ test('passes with DateTime and DateTimeImmutable', function () { expect($now)->not->toBeLessThanOrEqual($past); }); +test('passes with strings', function () { + expect('a')->toBeLessThanOrEqual('b'); + expect('a')->toBeLessThanOrEqual('a'); +}); + test('failures', function () { expect(4)->toBeLessThanOrEqual(3.9); })->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeStringBackedEnum.php b/tests/Features/Expect/toBeStringBackedEnum.php new file mode 100644 index 00000000..4451185d --- /dev/null +++ b/tests/Features/Expect/toBeStringBackedEnum.php @@ -0,0 +1,9 @@ +expect('Tests\Fixtures\Arch\ToBeStringBackedEnum\HasStringBacking') + ->toBeStringBackedEnum(); + +test('enum is not backed by string') + ->expect('Tests\Fixtures\Arch\ToBeStringBackedEnum\HasIntBacking') + ->not->toBeStringBackedEnum(); diff --git a/tests/Features/Expect/toContainEqual.php b/tests/Features/Expect/toContainEqual.php new file mode 100644 index 00000000..796dadd7 --- /dev/null +++ b/tests/Features/Expect/toContainEqual.php @@ -0,0 +1,35 @@ +toContainEqual('42'); +}); + +test('passes arrays with multiple needles', function () { + expect([1, 2, 42])->toContainEqual('42', '2'); +}); + +test('failures', function () { + expect([1, 2, 42])->toContainEqual('3'); +})->throws(ExpectationFailedException::class); + +test('failures with multiple needles (all failing)', function () { + expect([1, 2, 42])->toContainEqual('3', '4'); +})->throws(ExpectationFailedException::class); + +test('failures with multiple needles (some failing)', function () { + expect([1, 2, 42])->toContainEqual('1', '3', '4'); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect([1, 2, 42])->not->toContainEqual('42'); +})->throws(ExpectationFailedException::class); + +test('not failures with multiple needles (all failing)', function () { + expect([1, 2, 42])->not->toContainEqual('42', '2'); +})->throws(ExpectationFailedException::class); + +test('not failures with multiple needles (some failing)', function () { + expect([1, 2, 42])->not->toContainEqual('42', '1'); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toMatchSnapshot.php b/tests/Features/Expect/toMatchSnapshot.php index 8196cc0b..6dc3e83e 100644 --- a/tests/Features/Expect/toMatchSnapshot.php +++ b/tests/Features/Expect/toMatchSnapshot.php @@ -21,6 +21,23 @@ test('pass', function () { expect($this->snapshotable)->toMatchSnapshot(); }); +expect()->pipe('toMatchSnapshot', function (Closure $next) { + if (is_string($this->value)) { + $this->value = preg_replace( + '/name="_token" value=".*"/', + 'name="_token" value="1"', + $this->value + ); + } + + return $next(); +}); + +test('pass using pipes', function () { + expect('') + ->toMatchSnapshot(); +}); + test('pass with `__toString`', function () { TestSuite::getInstance()->snapshots->save($this->snapshotable); diff --git a/tests/Fixtures/Arch/ToBeIntBackedEnum/HasIntBacking/HasIntBackingEnum.php b/tests/Fixtures/Arch/ToBeIntBackedEnum/HasIntBacking/HasIntBackingEnum.php new file mode 100644 index 00000000..eddac03f --- /dev/null +++ b/tests/Fixtures/Arch/ToBeIntBackedEnum/HasIntBacking/HasIntBackingEnum.php @@ -0,0 +1,10 @@ +afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) - ->toBe(0); + ->toBe(1); - $this->ith = 1; + $this->ith = 2; }); afterEach(function () { expect($this) ->toHaveProperty('ith') ->and($this->ith) - ->toBe(1); + ->toBe(2); }); test('global afterEach execution order', function () { diff --git a/tests/Hooks/BeforeEachTest.php b/tests/Hooks/BeforeEachTest.php index a9317cef..9b74ee10 100644 --- a/tests/Hooks/BeforeEachTest.php +++ b/tests/Hooks/BeforeEachTest.php @@ -1,15 +1,6 @@ beforeEach(function () { - expect($this) - ->toHaveProperty('baz') - ->and($this->baz) - ->toBe(0); - - $this->baz = 1; -}); - -beforeEach(function () { expect($this) ->toHaveProperty('baz') ->and($this->baz) @@ -18,9 +9,18 @@ beforeEach(function () { $this->baz = 2; }); -test('global beforeEach execution order', function () { +beforeEach(function () { expect($this) ->toHaveProperty('baz') ->and($this->baz) ->toBe(2); + + $this->baz = 3; +}); + +test('global beforeEach execution order', function () { + expect($this) + ->toHaveProperty('baz') + ->and($this->baz) + ->toBe(3); }); diff --git a/tests/Overrides/VersionsTest.php b/tests/Overrides/VersionsTest.php new file mode 100644 index 00000000..1fdd63c0 --- /dev/null +++ b/tests/Overrides/VersionsTest.php @@ -0,0 +1,18 @@ +toBe($expectedHash); +})->with(function () { + foreach (BootOverrides::FILES as $hash => $file) { + $path = implode(DIRECTORY_SEPARATOR, [ + dirname(__DIR__, 2), + 'vendor/phpunit/phpunit/src', + $file, + ]); + yield $file => [$path, $hash]; + } +}); diff --git a/tests/Pest.php b/tests/Pest.php index 71751ac4..6f40b03e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -3,6 +3,8 @@ use Tests\CustomTestCase\CustomTestCase; use Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder\CustomTestCaseInSubFolder; +$GLOBALS['__PEST_INTERNAL_TEST_SUITE'] = true; + uses(CustomTestCaseInSubFolder::class)->in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder'); // test case for all the directories inside PHPUnit/GlobPatternTests/SubFolder/ @@ -33,9 +35,49 @@ uses() }) ->in('Hooks'); +uses() + ->beforeEach(function () { + expect($this) + ->toHaveProperty('baz') + ->and($this->baz) + ->toBe(0); + + $this->baz = 1; + }) + ->beforeAll(function () { + expect($_SERVER['globalHook']) + ->toHaveProperty('beforeAll') + ->and($_SERVER['globalHook']->beforeAll) + ->toBe(0); + + $_SERVER['globalHook']->beforeAll = 1; + }) + ->afterEach(function () { + expect($this) + ->toHaveProperty('ith') + ->and($this->ith) + ->toBe(0); + + $this->ith = 1; + }) + ->afterAll(function () { + expect($_SERVER['globalHook']) + ->toHaveProperty('afterAll') + ->and($_SERVER['globalHook']->afterAll) + ->toBe(0); + + $_SERVER['globalHook']->afterAll = 1; + }) + ->in('Hooks'); + function helper_returns_string() { return 'string'; } dataset('dataset_in_pest_file', ['A', 'B']); + +function removeAnsiEscapeSequences(string $input): ?string +{ + return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $input); +} diff --git a/tests/Visual/Collision.php b/tests/Visual/Collision.php index 04edbc09..7812fc0d 100644 --- a/tests/Visual/Collision.php +++ b/tests/Visual/Collision.php @@ -10,7 +10,7 @@ test('collision', function (array $arguments) { $process->run(); - return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); + return removeAnsiEscapeSequences($process->getOutput()); }; $outputContent = explode("\n", $output()); diff --git a/tests/Visual/Help.php b/tests/Visual/Help.php index 4f48171d..eea3fd99 100644 --- a/tests/Visual/Help.php +++ b/tests/Visual/Help.php @@ -6,7 +6,7 @@ test('visual snapshot of help command output', function () { $process->run(); - return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); + return removeAnsiEscapeSequences($process->getOutput()); }; expect($output())->toMatchSnapshot(); diff --git a/tests/Visual/JUnit.php b/tests/Visual/JUnit.php new file mode 100644 index 00000000..59562b1d --- /dev/null +++ b/tests/Visual/JUnit.php @@ -0,0 +1,79 @@ + 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'], + ); + + $process->run(); + + $rawXmlContent = file_get_contents($junitLogFile); + unlink($junitLogFile); + + // convert xml to array + try { + $xml = new SimpleXMLElement(preg_replace("/(<\/?)(\w+):([^>]*>)/", '$1$2$3', $rawXmlContent)); + + return json_decode(json_encode((array) $xml), true); + } catch (Exception $exception) { + throw new XmlParseException($exception->getMessage(), $exception->getCode(), $exception->getPrevious()); + } +}; + +$normalizedPath = function (string $path) { + return str_replace('/', DIRECTORY_SEPARATOR, $path); +}; + +test('junit output', function () use ($normalizedPath, $run) { + $result = $run('tests/.tests/SuccessOnly.php'); + + expect($result['testsuite']['@attributes']) + ->name->toBe('Tests\tests\SuccessOnly') + ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php')) + ->tests->toBe('2') + ->assertions->toBe('2') + ->errors->toBe('0') + ->failures->toBe('0') + ->skipped->toBe('0'); + + expect($result['testsuite']['testcase']) + ->toHaveCount(2); + + expect($result['testsuite']['testcase'][0]['@attributes']) + ->name->toBe('it can pass with comparison') + ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php::it can pass with comparison')) + ->class->toBe('Tests\tests\SuccessOnly') + ->classname->toBe('Tests.tests.SuccessOnly') + ->assertions->toBe('1') + ->time->toStartWith('0.0'); +}); + +test('junit with parallel', function () use ($normalizedPath, $run) { + $result = $run('tests/.tests/SuccessOnly.php', '--parallel', '--processes=1', '--filter', 'can pass with comparison'); + + expect($result['testsuite']['@attributes']) + ->name->toBe('Tests\tests\SuccessOnly') + ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php')) + ->tests->toBe('1') + ->assertions->toBe('1') + ->errors->toBe('0') + ->failures->toBe('0') + ->skipped->toBe('0'); + + expect($result['testsuite']['testcase']) + ->toHaveCount(1); + + expect($result['testsuite']['testcase']['@attributes']) + ->name->toBe('it can pass with comparison') + ->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php::it can pass with comparison')) + ->class->toBe('Tests\tests\SuccessOnly') + ->classname->toBe('Tests.tests.SuccessOnly') + ->assertions->toBe('1') + ->time->toStartWith('0.0'); +}); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index a91e2bb3..767acf32 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -13,12 +13,12 @@ $run = function () { $process->run(); - return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); + return removeAnsiEscapeSequences($process->getOutput()); }; test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 16 skipped, 965 passed (2285 assertions)') + ->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 16 skipped, 994 passed (2348 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); diff --git a/tests/Visual/SingleTestOrDirectory.php b/tests/Visual/SingleTestOrDirectory.php index 21223275..63e29490 100644 --- a/tests/Visual/SingleTestOrDirectory.php +++ b/tests/Visual/SingleTestOrDirectory.php @@ -9,7 +9,7 @@ $run = function (string $target, $decorated = false) { $process->run(); - return $decorated ? $process->getOutput() : preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); + return $decorated ? $process->getOutput() : removeAnsiEscapeSequences($process->getOutput()); }; $snapshot = function ($name) { diff --git a/tests/Visual/Todo.php b/tests/Visual/Todo.php index 9e9461b1..7c11e07c 100644 --- a/tests/Visual/Todo.php +++ b/tests/Visual/Todo.php @@ -11,9 +11,7 @@ $run = function (string $target, bool $parallel) { expect($process->getExitCode())->toBe(0); - $outputContent = preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); - - return $outputContent; + return removeAnsiEscapeSequences($process->getOutput()); }; $snapshot = function ($name) { diff --git a/tests/Visual/Version.php b/tests/Visual/Version.php index 50b156b8..abdb6cbf 100644 --- a/tests/Visual/Version.php +++ b/tests/Visual/Version.php @@ -6,7 +6,7 @@ test('visual snapshot of help command output', function () { $process->run(); - return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); + return removeAnsiEscapeSequences($process->getOutput()); }; expect($output())->toMatchSnapshot();