Compare commits

...

7 Commits

Author SHA1 Message Date
nuno maduro 962510b7e3 chore: adjusts snapshots 2026-07-21 01:14:59 +01:00
Punyapal Shah e9c268a584 feat: validation expectations (#1762)
* feat: add validation expectations for IP, MAC, hostname, domain, base64, and hexadecimal

* improvements

* improvements
2026-07-20 11:21:57 +01:00
nuno maduro fa91fcf744 fix: coverage merger 2026-07-19 16:45:06 +01:00
nuno maduro c6073511a2 chore: fixes test printer 2026-07-19 13:59:31 +01:00
nuno maduro b5e1b83bb4 fix: cache results on parallel 2026-07-19 12:29:33 +01:00
nuno maduro 1c11595504 chore: moves to coding stlye 2026-07-19 03:41:57 +01:00
nuno maduro cd0e921158 chore: style 2026-07-19 00:42:29 +01:00
40 changed files with 673 additions and 69 deletions
@@ -0,0 +1,71 @@
<?php
/*
* The MIT License (MIT)
*
* Copyright (c) 2011 Brian Scaturro
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
declare(strict_types=1);
namespace ParaTest\WrapperRunner;
use PHPUnit\TextUI\Output\Printer;
use function preg_match;
/**
* @internal
*
* This file is overridden so the "T" progress character — emitted by Pest for
* "todo" tests — is routed to the progress file next to the regular characters,
* instead of being treated as unexpected output.
*/
final readonly class ProgressPrinterOutput implements Printer
{
public function __construct(
private Printer $progressPrinter,
private Printer $outputPrinter,
) {}
public function print(string $buffer): void
{
// Skip anything in \PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter::printProgress except $progress
if (
$buffer === "\n"
|| preg_match('/^ +$/', $buffer) === 1
|| preg_match('/^ \d+ \/ \d+ \(...%\)$/', $buffer) === 1
) {
return;
}
match ($buffer) {
'E', 'F', 'I', 'N', 'D', 'R', 'W', 'S', 'T', '.' => $this->progressPrinter->print($buffer),
default => $this->outputPrinter->print($buffer),
};
}
public function flush(): void
{
$this->progressPrinter->flush();
$this->outputPrinter->flush();
}
}
@@ -43,7 +43,7 @@ declare(strict_types=1);
* file that was distributed with this source code.
*/
namespace Pest\Logging\TeamCity\Subscriber;
namespace PHPUnit\TextUI\Output\Default\ProgressPrinter;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber;
@@ -51,16 +51,20 @@ use ReflectionClass;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*
* This file is overridden so PHPUnit's progress output emits a "T" before the
* regular "S" for "todo" tests — Pest's parallel result printer consumes the
* "T" and swallows the "S" that follows it.
*/
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
final readonly class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{
public function notify(Skipped $event): void
{
if (str_contains($event->message(), '__TODO__')) {
if ($event->message() === '__TODO__') {
$this->printTodoItem();
}
$this->logger()->testSkipped($event);
$this->printer()->testSkipped();
}
/**
@@ -69,7 +73,7 @@ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscribe
private function printTodoItem(): void
{
$mirror = new ReflectionClass($this->printer());
$printerMirror = $mirror->getMethod('printProgress');
$printerMirror->invoke($this->printer(), 'T');
$printProgress = $mirror->getMethod('printProgress');
$printProgress->invoke($this->printer(), 'T');
}
}
+1 -2
View File
@@ -23,8 +23,7 @@ return RectorConfig::configure()
__DIR__.'/tests',
])
->withSets([
PestSetList::PEST_CODE_QUALITY,
PestSetList::PEST_CHAIN,
PestSetList::CODING_STYLE,
])
->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
+1
View File
@@ -18,6 +18,7 @@ final class BootOverrides implements Bootstrapper
* @var array<int, string>
*/
public const array FILES = [
'ParaTest/WrapperRunner/ProgressPrinterOutput.php',
'Runner/Filter/NameFilterIterator.php',
'Runner/ResultCache/DefaultResultCache.php',
'Runner/TestSuiteLoader.php',
+20 -21
View File
@@ -15,11 +15,6 @@ use PHPUnit\Event\Test\AfterLastTestMethodErrored;
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
use PHPUnit\Event\Test\BeforeFirstTestMethodFailed;
use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestSuite\TestSuite;
use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider;
use PHPUnit\Framework\Exception as FrameworkException;
@@ -254,25 +249,29 @@ final readonly class Converter
...$result->testMarkedIncompleteEvents(),
];
$numberOfNotPassedTests = count(
array_unique(
array_map(
function (AfterLastTestMethodErrored|AfterLastTestMethodFailed|BeforeFirstTestMethodErrored|BeforeFirstTestMethodFailed|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string {
if ($event instanceof BeforeFirstTestMethodErrored
|| $event instanceof AfterLastTestMethodErrored
|| $event instanceof BeforeFirstTestMethodFailed
|| $event instanceof AfterLastTestMethodFailed) {
return $event->testClassName();
$notPassedTests = [];
foreach ($events as $event) {
if ($event instanceof AfterLastTestMethodErrored) {
// PHPUnit's collector does not count these towards `numberOfTestsRun`...
continue;
}
if ($event instanceof AfterLastTestMethodFailed) {
// PHPUnit's collector does not count these towards `numberOfTestsRun`...
continue;
}
if ($event instanceof BeforeFirstTestMethodErrored || $event instanceof BeforeFirstTestMethodFailed) {
$notPassedTests[] = $event->testClassName();
continue;
}
return $this->getTestCaseLocation($event->test());
},
$events
)
)
);
$notPassedTests[] = $this->getTestCaseLocation($event->test());
}
$numberOfPassedTests = $result->numberOfTestsRun() - $numberOfNotPassedTests;
$numberOfPassedTests = $result->numberOfTestsRun()
- count(array_unique($notPassedTests))
- $result->numberOfTestSkippedByTestSuiteSkippedEvents();
return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result);
}
@@ -14,6 +14,10 @@ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscribe
{
public function notify(Skipped $event): void
{
if ($event->message() === '__TODO__') {
return; // "todo" tests are reported in the summary, not as ignored tests...
}
$this->logger()->testSkipped($event);
}
}
+100
View File
@@ -1219,4 +1219,104 @@ final class Expectation
return $this;
}
/**
* Asserts that the value is an IP address.
*
* @return self<TValue>
*/
public function toBeIpAddress(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
Assert::assertTrue((bool) filter_var($this->value, FILTER_VALIDATE_IP), $message);
return $this;
}
/**
* Asserts that the value is a MAC address.
*
* @return self<TValue>
*/
public function toBeMacAddress(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
Assert::assertTrue((bool) filter_var($this->value, FILTER_VALIDATE_MAC), $message);
return $this;
}
/**
* Asserts that the value is a hostname.
*
* @return self<TValue>
*/
public function toBeHostname(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
Assert::assertTrue((bool) filter_var($this->value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME), $message);
return $this;
}
/**
* Asserts that the value is a domain name.
*
* @return self<TValue>
*/
public function toBeDomain(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
$isValid = filter_var($this->value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false
&& str_contains($this->value, '.');
Assert::assertTrue($isValid, $message);
return $this;
}
/**
* Asserts that the value is a base64-encoded string.
*
* @return self<TValue>
*/
public function toBeBase64(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
$decoded = base64_decode($this->value, true);
Assert::assertTrue($decoded !== false && base64_encode($decoded) === $this->value, $message);
return $this;
}
/**
* Asserts that the value is a hexadecimal string.
*
* @return self<TValue>
*/
public function toBeHexadecimal(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
Assert::assertTrue(ctype_xdigit($this->value), $message);
return $this;
}
}
@@ -120,10 +120,6 @@ final class ResultPrinter
$unexpectedOutput = $this->tail($outputFile);
if ($unexpectedOutput !== '') {
if (preg_match('/^T+$/', $unexpectedOutput) > 0) {
return;
}
$this->output->write($unexpectedOutput);
}
@@ -11,6 +11,7 @@ use ParaTest\JUnit\LogMerger;
use ParaTest\JUnit\Writer;
use ParaTest\Options;
use ParaTest\RunnerInterface;
use ParaTest\WrapperRunner\MissingResultsException;
use ParaTest\WrapperRunner\SuiteLoader;
use ParaTest\WrapperRunner\WrapperWorker;
use Pest\Result;
@@ -42,6 +43,8 @@ use function assert;
use function count;
use function dirname;
use function file_get_contents;
use function filesize;
use function is_file;
use function max;
use function realpath;
use function str_starts_with;
@@ -54,14 +57,14 @@ use function usleep;
*/
final class WrapperRunner implements RunnerInterface
{
/**
* The time to sleep between cycles.
*/
/**
* The merged test result from the parallel run.
*/
public static ?TestResult $result = null;
/**
* The time to sleep between cycles.
*/
private const int CYCLE_SLEEP = 10000;
/**
@@ -88,6 +91,18 @@ final class WrapperRunner implements RunnerInterface
/** @var array<int,int> */
private array $batches = [];
/** @var array<non-empty-string,true> */
private array $requiredTestResultFiles = [];
/** @var array<non-empty-string,true> */
private array $requiredCoverageFiles = [];
/** @var list<SplFileInfo> */
private array $statusFiles = [];
/** @var list<SplFileInfo> */
private array $progressFiles = [];
/** @var list<SplFileInfo> */
private array $unexpectedOutputFiles = [];
@@ -217,7 +232,7 @@ final class WrapperRunner implements RunnerInterface
if (
$this->exitcode > 0
&& $this->options->configuration->stopOnFailure()
&& $this->options->configuration->stopOnFailureThreshold() > 0
) {
$this->pending = [];
} elseif (($pending = array_shift($this->pending)) !== null) {
@@ -232,6 +247,18 @@ final class WrapperRunner implements RunnerInterface
private function flushWorker(WrapperWorker $worker): void
{
if ($worker->hasExecutedTests()) {
$testResultFile = $worker->testResultFile->getPathname();
if ($testResultFile !== '') {
$this->requiredTestResultFiles[$testResultFile] = true;
}
if (isset($worker->coverageFile) && $worker->coverageFile->getPathname() !== '') {
$this->requiredCoverageFiles[$worker->coverageFile->getPathname()] = true;
}
}
$this->exitcode = max($this->exitcode, $worker->getExitCode());
$this->printer->printFeedback(
$worker->progressFile,
@@ -279,10 +306,15 @@ final class WrapperRunner implements RunnerInterface
$worker->start();
$this->batches[$token] = 0;
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->statusFiles[] = $worker->statusFile;
$this->progressFiles[] = $worker->progressFile;
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->testResultFiles[] = $worker->testResultFile;
if (isset($worker->resultCacheFile)) {
$this->resultCacheFiles[] = $worker->resultCacheFile;
}
if (isset($worker->junitFile)) {
$this->junitFiles[] = $worker->junitFile;
}
@@ -315,6 +347,20 @@ final class WrapperRunner implements RunnerInterface
private function complete(TestResult $testResultSum): int
{
$missingTestResultFiles = [];
foreach ($this->requiredTestResultFiles as $filePath => $true) {
if (is_file($filePath)) {
continue;
}
$missingTestResultFiles[] = $filePath;
}
if ($missingTestResultFiles !== []) {
throw MissingResultsException::create($missingTestResultFiles, 'test_result');
}
foreach ($this->testResultFiles as $testResultFile) {
if (! $testResultFile->isFile()) {
continue;
@@ -398,7 +444,7 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->testRunnerTriggeredNoticeEvents(),
array_values(array_filter(
$testResultSum->testRunnerTriggeredWarningEvents(),
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found in class')
)),
$testResultSum->testRunnerTriggeredIssueDeprecationEvents(),
$testResultSum->testRunnerTriggeredIssueErrorEvents(),
@@ -442,8 +488,11 @@ final class WrapperRunner implements RunnerInterface
$exitcode = Result::exitCode($this->options->configuration, $testResultSum);
$this->clearFiles($this->statusFiles);
$this->clearFiles($this->progressFiles);
$this->clearFiles($this->unexpectedOutputFiles);
$this->clearFiles($this->testResultFiles);
$this->clearFiles($this->resultCacheFiles);
$this->clearFiles($this->coverageFiles);
$this->clearFiles($this->junitFiles);
$this->clearFiles($this->teamcityFiles);
@@ -458,6 +507,20 @@ final class WrapperRunner implements RunnerInterface
return;
}
$missingCoverageFiles = [];
foreach ($this->requiredCoverageFiles as $filePath => $true) {
if (is_file($filePath) && filesize($filePath) !== 0) {
continue;
}
$missingCoverageFiles[] = $filePath;
}
if ($missingCoverageFiles !== []) {
throw MissingResultsException::create($missingCoverageFiles, 'coverage');
}
$coverageManager = new CodeCoverage;
$coverageManager->init(
$this->options->configuration,
+57 -1
View File
@@ -8,6 +8,9 @@ use Pest\Plugins\Tia;
use Pest\Plugins\Tia\Contracts\State;
use Pest\Support\Container;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\Selector;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\Serialization\Unserializer;
use Throwable;
/**
@@ -161,7 +164,60 @@ final class CoverageMerger
return null;
}
return $value instanceof CodeCoverage ? $value : null;
// Legacy `--coverage-php` format: a serialized `CodeCoverage` object.
if ($value instanceof CodeCoverage) {
return $value;
}
// Since phpunit/php-code-coverage 14, `--coverage-php` writes the report
// as a serialized array (`['codeCoverage' => ..., 'testResults' => ...,
// 'basePath' => ...]`) rather than a `CodeCoverage` object, so it has to
// be rebuilt into one before it can be merged.
return self::coverageFromSerializedData($reportPath);
}
private static function coverageFromSerializedData(string $reportPath): ?CodeCoverage
{
if ($reportPath === '') {
return null;
}
try {
$serialized = new Unserializer()->unserialize($reportPath);
} catch (Throwable) {
return null;
}
$data = $serialized['codeCoverage'];
$basePath = $serialized['basePath'];
if ($basePath !== '') {
foreach ($data->coveredFiles() as $relativePath) {
$data->renameFile($relativePath, $basePath.DIRECTORY_SEPARATOR.$relativePath);
}
}
$coverage = self::emptyCoverage();
if (! $coverage instanceof CodeCoverage) {
return null;
}
$coverage->setData($data);
$coverage->setTests($serialized['testResults']);
return $coverage;
}
private static function emptyCoverage(): ?CodeCoverage
{
try {
$filter = new Filter;
return new CodeCoverage(new Selector()->forLineCoverage($filter), $filter);
} catch (Throwable) {
return null;
}
}
private static function unserializeCoverage(string $bytes): ?CodeCoverage
+36 -4
View File
@@ -10,6 +10,10 @@ use NunoMaduro\Collision\Exceptions\TestOutcome;
use PHPUnit\Event\Code\TestDoxBuilder;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\ThrowableBuilder;
use PHPUnit\Event\Test\AfterLastTestMethodErrored;
use PHPUnit\Event\Test\AfterLastTestMethodFailed;
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
use PHPUnit\Event\Test\BeforeFirstTestMethodFailed;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\PhpunitDeprecationTriggered;
@@ -35,8 +39,7 @@ final class StateGenerator
$testResultEvent->throwable()
));
} else {
// @phpstan-ignore-next-line
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
$this->addClassLevelEvent($state, $testResultEvent);
}
}
@@ -48,8 +51,7 @@ final class StateGenerator
$testResultEvent->throwable()
));
} else {
// @phpstan-ignore-next-line
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
$this->addClassLevelEvent($state, $testResultEvent);
}
}
@@ -184,6 +186,36 @@ final class StateGenerator
return $state;
}
/**
* Adds the given class-level "hook" failure to the state. Collision's
* `fromBeforeFirstTestMethodErrored` only accepts `BeforeFirstTestMethodErrored`
* events, so the remaining class-level events get a synthesized test method.
*/
private function addClassLevelEvent(State $state, AfterLastTestMethodErrored|AfterLastTestMethodFailed|BeforeFirstTestMethodErrored|BeforeFirstTestMethodFailed $event): void
{
if ($event instanceof BeforeFirstTestMethodErrored) {
$state->add(TestResult::fromBeforeFirstTestMethodErrored($event));
return;
}
$methodName = $event instanceof BeforeFirstTestMethodFailed ? 'beforeAll' : 'afterAll';
$state->add(TestResult::fromPestParallelTestCase(
new TestMethod(
$event->testClassName(), // @phpstan-ignore-line
$methodName,
'', // @phpstan-ignore-line
1,
TestDoxBuilder::fromClassNameAndMethodName($event->testClassName(), $methodName), // @phpstan-ignore-line
MetadataCollection::fromArray([]),
TestDataCollection::fromArray([])
),
TestResult::FAIL,
$event->throwable()
));
}
/**
* @param array<string, list<PhpunitDeprecationTriggered|PhpunitErrorTriggered|PhpunitNoticeTriggered|PhpunitWarningTriggered>> $testResultEvents
*/
+51 -1
View File
@@ -476,6 +476,16 @@
✓ pass
✓ failures
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeBase64
✓ pass
✓ failures
✓ failures with invalid padding
✓ failures with padding only
✓ failures with malformed input
✓ failures with invalid type
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeBetween
@@ -522,6 +532,14 @@
✓ pass
✓ failures
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeDomain
✓ pass
✓ failures
✓ failures with leading dot
✓ failures with invalid type
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeEmail
@@ -584,6 +602,24 @@
✓ passes with strings
✓ failures
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeHexadecimal
✓ pass
✓ failures
✓ failures with invalid characters
✓ failures with zero-x prefix
✓ failures with empty string
✓ failures with invalid type
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeHostname
✓ pass
✓ failures
✓ failures with trailing hyphen
✓ failures with invalid type
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeIn
@@ -622,6 +658,13 @@
✓ failure when the class is not invokable
✓ class is not invokable
PASS Tests\Features\Expect\toBeIpAddress
✓ pass
✓ failures
✓ failures with invalid type
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeIterable
✓ pass
✓ failures
@@ -666,6 +709,13 @@
✓ pass
✓ failures
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeMacAddress
✓ pass
✓ failures
✓ failures with invalid type
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeNAN
@@ -2136,4 +2186,4 @@
✓ pass with dataset with ('my-datas-set-value')
✓ within describe → pass with dataset with ('my-datas-set-value')
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1504 passed (3281 assertions)
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1542 passed (3375 assertions)
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Pest\Expectation;
use Pest\Plugins\Tia\BaselineSync;
+4 -2
View File
@@ -1,12 +1,14 @@
<?php
declare(strict_types=1);
use Pest\Plugin;
trait PluginTrait
{
public function assertPluginTraitGotRegistered(): void
{
$this->assertTrue(true);
expect(true)->toBeTrue();
}
}
@@ -14,7 +16,7 @@ trait SecondPluginTrait
{
public function assertSecondPluginTraitGotRegistered(): void
{
$this->assertTrue(true);
expect(true)->toBeTrue();
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
$file = __DIR__.DIRECTORY_SEPARATOR.'after-all-test';
beforeAll(function () use ($file): void {
@@ -12,7 +14,7 @@ afterAll(function () use ($file): void {
test('deletes file after all', function () use ($file): void {
file_put_contents($file, 'foo');
$this->assertFileExists($file);
expect($file)->toBeFile();
register_shutdown_function(function (): void {
// $this->assertFileDoesNotExist($file);
});
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
beforeEach()->expect(true)->toBeTrue();
test('runs 1', function (): void {
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
beforeEach()->skip();
test('does not run 1', function (): void {
+3 -8
View File
@@ -10,17 +10,12 @@ beforeEach(function (): void {
});
it('throws exception if dataset does not exist', function (): void {
$this->expectException(DatasetDoesNotExist::class);
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
DatasetsRepository::resolve(['first'], __FILE__);
expect(fn () => DatasetsRepository::resolve(['first'], __FILE__))->toThrow(DatasetDoesNotExist::class, "A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
});
it('throws exception if dataset already exist', function (): void {
DatasetsRepository::set('second', [[]], __DIR__);
$this->expectException(DatasetAlreadyExists::class);
$this->expectExceptionMessage('A dataset with the name `second` already exists in scope ['.__DIR__.'].');
DatasetsRepository::set('second', [[]], __DIR__);
expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset with the name `second` already exists in scope ['.__DIR__.'].');
});
it('sets closures', function (): void {
@@ -38,7 +33,7 @@ it('sets arrays', function (): void {
});
it('gets bound to test case object', function ($value): void {
$this->assertTrue(true);
expect(true)->toBeTrue();
})->with([['a'], ['b']]);
test('it truncates the description', function (): void {
+3 -5
View File
@@ -1,9 +1,7 @@
<?php
it('gives access the the underlying expectException', function (): void {
$this->expectException(InvalidArgumentException::class);
throw new InvalidArgumentException;
expect(fn () => throw new InvalidArgumentException)->toThrow(InvalidArgumentException::class);
});
it('catch exceptions', function (): void {
@@ -27,7 +25,7 @@ it('can just define the code', function (): void {
})->throws(1);
it('not catch exceptions if given condition is false', function (): void {
$this->assertTrue(true);
expect(true)->toBeTrue();
})->throwsIf(false, Exception::class);
it('catch exceptions if given condition is true', function (): void {
@@ -59,7 +57,7 @@ it('can just define the code if given condition is 1', function (): void {
})->throwsIf(1, 1);
it('not catch exceptions if given condition is true', function (): void {
$this->assertTrue(true);
expect(true)->toBeTrue();
})->throwsUnless(true, Exception::class);
it('catch exceptions if given condition is false', function (): void {
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
use Pest\Exceptions\InvalidExpectationValue;
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function (): void {
expect('Zm9v')->toBeBase64() // 'foo' (no padding)
->and('Zm9vYg==')->toBeBase64() // 'foob' (with padding)
->and('Zm9vYmE=')->toBeBase64() // 'fooba' (with padding)
->and('Zm9vYmFy')->toBeBase64() // 'foobar' (no padding)
->and('')->toBeBase64(); // empty string
});
test('failures', function (): void {
expect('not-base64!')->toBeBase64();
})->throws(ExpectationFailedException::class);
test('failures with invalid padding', function (): void {
expect('AAA')->toBeBase64();
})->throws(ExpectationFailedException::class);
test('failures with padding only', function (): void {
expect('====')->toBeBase64();
})->throws(ExpectationFailedException::class);
test('failures with malformed input', function (): void {
expect('!!invalid!!')->toBeBase64();
})->throws(ExpectationFailedException::class);
test('failures with invalid type', function (): void {
expect([])->toBeBase64();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].');
test('failures with custom message', function (): void {
expect('!!invalid!!')->toBeBase64('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function (): void {
expect('Zm9v')->not->toBeBase64();
})->throws(ExpectationFailedException::class);
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Pest\Exceptions\InvalidExpectationValue;
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function (): void {
expect('example.com')->toBeDomain() // standard domain
->and('sub.example.com')->toBeDomain() // subdomain
->and('my-host.io')->toBeDomain() // with hyphen
->and('example.co.uk')->toBeDomain(); // multi-level TLD
});
test('failures', function (): void {
expect('example')->toBeDomain();
})->throws(ExpectationFailedException::class);
test('failures with leading dot', function (): void {
expect('.example.com')->toBeDomain();
})->throws(ExpectationFailedException::class);
test('failures with invalid type', function (): void {
expect([])->toBeDomain();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].');
test('failures with custom message', function (): void {
expect('example')->toBeDomain('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function (): void {
expect('example.com')->not->toBeDomain();
})->throws(ExpectationFailedException::class);
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
use Pest\Exceptions\InvalidExpectationValue;
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function (): void {
expect('abcdef')->toBeHexadecimal() // lowercase
->and('ABCDEF')->toBeHexadecimal() // uppercase
->and('aBcDeF')->toBeHexadecimal() // mixed case
->and('1234567890')->toBeHexadecimal() // numeric
->and('deadbeef')->toBeHexadecimal(); // alphanumeric
});
test('failures', function (): void {
expect('xyz')->toBeHexadecimal();
})->throws(ExpectationFailedException::class);
test('failures with invalid characters', function (): void {
expect('ghijkl')->toBeHexadecimal();
})->throws(ExpectationFailedException::class);
test('failures with zero-x prefix', function (): void {
expect('0x1a')->toBeHexadecimal();
})->throws(ExpectationFailedException::class);
test('failures with empty string', function (): void {
expect('')->toBeHexadecimal();
})->throws(ExpectationFailedException::class);
test('failures with invalid type', function (): void {
expect([])->toBeHexadecimal();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].');
test('failures with custom message', function (): void {
expect('xyz')->toBeHexadecimal('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function (): void {
expect('abcdef')->not->toBeHexadecimal();
})->throws(ExpectationFailedException::class);
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Pest\Exceptions\InvalidExpectationValue;
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function (): void {
expect('example')->toBeHostname() // single label
->and('example.com')->toBeHostname() // multiple labels
->and('sub.example.com')->toBeHostname() // subdomain
->and('my-host')->toBeHostname(); // with hyphen
});
test('failures', function (): void {
expect('-example')->toBeHostname();
})->throws(ExpectationFailedException::class);
test('failures with trailing hyphen', function (): void {
expect('example-')->toBeHostname();
})->throws(ExpectationFailedException::class);
test('failures with invalid type', function (): void {
expect([])->toBeHostname();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].');
test('failures with custom message', function (): void {
expect('-example')->toBeHostname('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function (): void {
expect('example.com')->not->toBeHostname();
})->throws(ExpectationFailedException::class);
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
test('enum is backed by int')
->expect('Tests\Fixtures\Arch\ToBeIntBackedEnum\HasIntBacking')
->toBeIntBackedEnum();
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use Pest\Exceptions\InvalidExpectationValue;
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function (): void {
expect('192.168.1.1')->toBeIpAddress()
->and('::1')->toBeIpAddress()
->and('2001:db8::1')->toBeIpAddress();
});
test('failures', function (): void {
expect('not-an-ip')->toBeIpAddress();
})->throws(ExpectationFailedException::class);
test('failures with invalid type', function (): void {
expect([])->toBeIpAddress();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].');
test('failures with custom message', function (): void {
expect('not-an-ip')->toBeIpAddress('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function (): void {
expect('192.168.1.1')->not->toBeIpAddress();
})->throws(ExpectationFailedException::class);
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use Pest\Exceptions\InvalidExpectationValue;
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function (): void {
expect('00:1a:2b:3c:4d:5e')->toBeMacAddress() // colon-separated
->and('00-1a-2b-3c-4d-5e')->toBeMacAddress() // hyphen-separated
->and('ff:ff:ff:ff:ff:ff')->toBeMacAddress();
});
test('failures', function (): void {
expect('not-a-mac')->toBeMacAddress();
})->throws(ExpectationFailedException::class);
test('failures with invalid type', function (): void {
expect([])->toBeMacAddress();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].');
test('failures with custom message', function (): void {
expect('not-a-mac')->toBeMacAddress('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function (): void {
expect('00:1a:2b:3c:4d:5e')->not->toBeMacAddress();
})->throws(ExpectationFailedException::class);
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
test('enum is backed by string')
->expect('Tests\Fixtures\Arch\ToBeStringBackedEnum\HasStringBacking')
->toBeStringBackedEnum();
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Pest\Arch\Exceptions\ArchExpectationFailedException;
use Tests\Fixtures\Arch\ToHaveAttribute\Attributes\AsAttribute;
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
$test_array = [
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Pest\Arch\Exceptions\ArchExpectationFailedException;
test('missing prefix')
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Pest\Arch\Exceptions\ArchExpectationFailedException;
test('missing suffix')
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\AssertionFailedError;
it('may fail', function (): void {
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
it('may fail', function (): void {
$this->fail();
})->fails();
+3 -1
View File
@@ -1,7 +1,9 @@
<?php
declare(strict_types=1);
test('a test', function (): void {
$this->assertArrayHasKey('key', ['key' => 'foo']);
expect(['key' => 'foo'])->toHaveKey('key');
});
test('higher order message test')->expect(true)->toBeTrue();
+1 -1
View File
@@ -11,6 +11,6 @@ class ExampleTest extends Base\ExampleTest
#[\Override]
public function test_example(): void
{
$this->assertTrue(true);
expect(true)->toBeTrue();
}
}
@@ -7,7 +7,7 @@ class MyCustomClassTest extends TestCase
{
public function assertTrueIsTrue(): void
{
$this->assertTrue(true);
expect(true)->toBeTrue();
}
}
@@ -10,6 +10,6 @@ class CustomTestCaseInSubFolder extends TestCase
{
public function assertCustomInSubFolderTrue(): void
{
$this->assertTrue(true);
expect(true)->toBeTrue();
}
}
@@ -14,7 +14,7 @@ abstract class MyCustomClass extends TestCase
{
public function assertTrueIsTrue(): void
{
$this->assertTrue(true);
expect(true)->toBeTrue();
}
}
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Pest\Plugins\Tia\ContentHash;
describe('of()', function (): void {
+2 -2
View File
@@ -24,13 +24,13 @@ test('parallel', function () use ($run): void {
$file = file_get_contents(__FILE__);
$file = preg_replace(
'/\$expected = \'.*?\';/',
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1487 passed (3228 assertions)';",
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1525 passed (3322 assertions)';",
$file,
);
file_put_contents(__FILE__, $file);
}
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1487 passed (3228 assertions)';
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1525 passed (3322 assertions)';
expect($output)
->toContain("Tests: {$expected}")