Compare commits

..

11 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
nuno maduro f8c4c061f8 fixes snapshots 2026-07-18 03:12:47 +01:00
nuno maduro 81eacd79fd fix: tia engine correctness issues
- keep table/Inertia/database link-tracking alive in piggyback-coverage
  recording, and merge link-only edges into the piggybacked edge set
- route migration changes through the watch-pattern fallback when the
  graph has no recorded table usage
- re-run cached failures whose test file cannot be located, downgrading
  filtered runs to full replay instead of silently skipping them
- align non-filtered replay with the failOn*/displayDetailsOn* rerun policy
- resolve anonymous index Blade components (<x-card> from card/index.blade.php)
  and stop swallowing components whose static usage walk selected no tests
- parse CTE/REPLACE queries and schema-qualified identifiers in TableExtractor
- scope-filter the xdebug recording path like pcov
- ignore in-flight .tmp files in FileState::keysWithPrefix and delete
  unreadable worker partials
- vite deps helper: resolve rolldown through rolldown-vite installs and
  degrade gracefully, honour vite.config resolve.alias (incl. the
  laravel-vite-plugin '@' default), resolve .vue/.svelte/.mts/.cts
  extensions, and stop memoizing cycle-tainted transitive sets

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 02:50:20 +01:00
nuno maduro 1ef680c75d fix: tia 2026-07-18 02:28:29 +01:00
nuno maduro 820fa08313 fix: package lock fingerprint 2026-07-18 01:10:01 +01:00
271 changed files with 4007 additions and 2220 deletions
+105 -6
View File
@@ -25,7 +25,21 @@ const PAGE_DIR_CANDIDATES = [
async function loadRolldown() {
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
const path = projectRequire.resolve('rolldown')
let path = null
try { path = projectRequire.resolve('rolldown') } catch {}
if (path === null) {
// rolldown-vite installs (vite@npm:rolldown-vite) ship rolldown as a
// dependency of the vite package rather than a top-level install.
try {
const viteRequire = createRequire(projectRequire.resolve('vite/package.json'))
path = viteRequire.resolve('rolldown')
} catch {}
}
if (path === null) return null
return await import(pathToFileURL(path).href)
}
@@ -90,6 +104,74 @@ export async function loadAliasFromTsconfig(projectRoot = PROJECT_ROOT) {
return alias
}
const VITE_CONFIG_FILES = [
'vite.config.ts',
'vite.config.js',
'vite.config.mjs',
'vite.config.cjs',
'vite.config.mts',
'vite.config.cts',
]
function resolveAliasTarget(projectRoot, target) {
if (target.startsWith('/')) {
// Vite resolves a leading slash against the project root, unless the
// config author used a genuinely absolute path.
return existsSync(target) ? target : resolve(projectRoot, '.' + target)
}
return resolve(projectRoot, target)
}
async function usesLaravelVitePlugin(projectRoot) {
const p = join(projectRoot, 'package.json')
if (!existsSync(p)) return false
try {
const pkg = JSON.parse(await readFile(p, 'utf8'))
return Boolean(pkg.dependencies?.['laravel-vite-plugin'] ?? pkg.devDependencies?.['laravel-vite-plugin'])
} catch {
return false
}
}
export async function loadAliasFromViteConfig(projectRoot = PROJECT_ROOT) {
const alias = {}
let source = null
for (const name of VITE_CONFIG_FILES) {
const p = join(projectRoot, name)
if (!existsSync(p)) continue
try { source = await readFile(p, 'utf8') } catch { continue }
break
}
if (source !== null) {
// The config is executable code we cannot import safely, so extract alias
// entries textually: a quoted `@…`/`~…` key, then the last quoted path in
// the value expression — covers `'@': '/resources/js'` as well as
// `'@': path.resolve(__dirname, 'resources/js')`. The value stops at a
// top-level comma; call arguments are kept via the paren group.
for (const m of source.matchAll(/['"`]([@~][\w./-]*)['"`]\s*:\s*((?:\([^)\n]*\)|[^,\n])*)/g)) {
const key = m[1]
if (alias[key] !== undefined) continue
const paths = [...m[2].matchAll(/['"`]([^'"`]+)['"`]/g)].map((p) => p[1])
const target = paths.length > 0 ? paths[paths.length - 1] : null
if (!target || target.includes('*')) continue
alias[key] = resolveAliasTarget(projectRoot, target)
}
}
// laravel-vite-plugin registers '@' → resources/js by default.
if (alias['@'] === undefined && (await usesLaravelVitePlugin(projectRoot))) {
alias['@'] = resolve(projectRoot, 'resources/js')
}
return alias
}
async function listPageFiles(pagesDir) {
if (!existsSync(pagesDir)) return []
@@ -153,8 +235,16 @@ async function main() {
return
}
const { rolldown } = await loadRolldown()
const alias = await loadAliasFromTsconfig()
const loaded = await loadRolldown()
if (loaded === null) {
process.stdout.write('{}')
return
}
const { rolldown } = loaded
// The vite config is what the dev server actually resolves with — let it win.
const alias = { ...(await loadAliasFromTsconfig()), ...(await loadAliasFromViteConfig()) }
const aliasKeys = Object.keys(alias)
const graph = new Map()
@@ -199,7 +289,7 @@ async function main() {
cwd: PROJECT_ROOT,
resolve: {
alias,
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json'],
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.mjs', '.cjs', '.json', '.vue', '.svelte'],
},
transform: { jsx: 'preserve' },
treeshake: false,
@@ -224,6 +314,11 @@ async function main() {
stack.add(id)
const acc = new Set()
// A set computed while skipping an in-stack (cyclic) dependency is missing
// that dependency's subtree and must not be memoized — the ancestor call
// completes it for the current traversal, but a cached copy would leak the
// incomplete set into other pages' traversals.
let complete = true
const deps = graph.get(id)
if (deps) {
for (const dep of deps) {
@@ -232,13 +327,17 @@ async function main() {
const rel = relative(PROJECT_ROOT, dep).split(sep).join('/')
acc.add(rel)
}
if (stack.has(dep)) continue
if (stack.has(dep)) {
complete = false
continue
}
const child = computeTransitive(dep, stack)
if (child) for (const r of child) acc.add(r)
if (!transitiveCache.has(dep)) complete = false
}
}
stack.delete(id)
transitiveCache.set(id, acc)
if (complete) transitiveCache.set(id, acc)
return acc
}
+2 -1
View File
@@ -58,10 +58,11 @@
]
},
"require-dev": {
"mrpunyapal/peststan": "^0.2.11",
"laravel/pao": "^1.1.2",
"pestphp/pest-dev-tools": "^5.0.0",
"pestphp/pest-plugin-browser": "^5.0.0",
"pestphp/pest-plugin-phpstan": "^5.0.0",
"pestphp/pest-plugin-rector": "^5.0.0",
"pestphp/pest-plugin-type-coverage": "^5.0.0",
"psy/psysh": "^0.12.24"
},
@@ -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 -1
View File
@@ -1,7 +1,7 @@
includes:
- phpstan-baseline.neon
- phpstan-pest-extension.neon
- vendor/mrpunyapal/peststan/extension.neon
- vendor/pestphp/pest-plugin-phpstan/extension.neon
parameters:
level: 7
+30
View File
@@ -2,21 +2,32 @@
declare(strict_types=1);
use Pest\Rector\Rules\UseToMatchArrayRector;
use Pest\Rector\Set\PestSetList;
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveReturnTagIncompatibleWithNativeTypeRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveUselessUnionReturnDocblockRector;
use Rector\DeadCode\Rector\Property\RemoveUnusedPrivatePropertyRector;
use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return RectorConfig::configure()
->withPaths([
__DIR__.'/src',
__DIR__.'/tests',
])
->withSets([
PestSetList::CODING_STYLE,
])
->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
__DIR__.'/tests/Fixtures/Arch',
ReturnNeverTypeRector::class,
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class,
@@ -26,6 +37,25 @@ return RectorConfig::configure()
RemoveReturnTagIncompatibleWithNativeTypeRector::class => [
__DIR__.'/src/Expectations/HigherOrderExpectation.php',
],
// Merges unrelated expectations into a single `toMatchArray()`, turning
// `toContain()` into exact matches, dropping `->not`, and mistaking a
// `toBeTrue()` failure message for an expected value. Unsafe here.
UseToMatchArrayRector::class,
// Test fixtures rely on "unused" constructors, params and properties
// (resolved via the container or read through reflection), so the
// dead-code and return-type rules below must not touch the test suite.
RemoveEmptyClassMethodRector::class => [
__DIR__.'/tests',
],
RemoveUnusedConstructorParamRector::class => [
__DIR__.'/tests',
],
RemoveUnusedPrivatePropertyRector::class => [
__DIR__.'/tests',
],
AddArrowFunctionReturnTypeRector::class => [
__DIR__.'/tests',
],
])
->withPreparedSets(
deadCode: true,
+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',
+21 -22
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 = [];
return $this->getTestCaseLocation($event->test());
},
$events
)
)
);
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();
$numberOfPassedTests = $result->numberOfTestsRun() - $numberOfNotPassedTests;
continue;
}
$notPassedTests[] = $this->getTestCaseLocation($event->test());
}
$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;
}
}
@@ -19,7 +19,7 @@ use PHPStan\Type\Type;
* $expectation, $opposite, $shouldReset) from being incorrectly resolved as
* higher-order value property accesses by downstream ExpressionTypeResolverExtensions.
*
* This extension must be registered BEFORE the peststan HigherOrderExpectationTypeExtension.
* This extension must be registered BEFORE the pest-plugin-phpstan HigherOrderExpectationTypeExtension.
*
* @internal
*/
@@ -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,
+45 -4
View File
@@ -296,7 +296,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$result = $this->replayGraph->getResult($this->branch, $testId);
if ($result instanceof TestStatus) {
if ($result->isFailure() || $result->isError()) {
if ($this->replayGraph->shouldRerunStatus($result)) {
$this->executedCount++;
return null;
@@ -402,7 +402,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$projectRoot = TestSuite::getInstance()->rootPath;
$perTest = $this->piggybackCoverage
? $this->coverageCollector->perTestFiles()
? $this->mergePerTestFiles($this->coverageCollector->perTestFiles(), $recorder->perTestFiles())
: $recorder->perTestFiles();
if ($perTest === []) {
@@ -562,7 +562,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return $exitCode;
}
$this->snapshotTestResults();
$this->snapshotTestResults(markKnownTestFiles: true);
return $exitCode;
}
@@ -715,6 +715,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
if ($this->piggybackCoverage) {
$this->recorder->activateLinkTracking();
$this->recordingActive = true;
return $arguments;
@@ -782,6 +783,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private function activateWorkerRecorderForReplay(array $arguments): array
{
if ($this->piggybackCoverage) {
$this->recorder->activateLinkTracking();
$this->recordingActive = true;
return $arguments;
@@ -834,6 +836,13 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$affectedFromChanges = $changed === [] ? [] : $graph->affected($changed);
$rerunFromCache = [];
if ($this->filteredMode && $graph->hasUnlocatedTestsToRerun($this->branch)) {
$this->filteredMode = false;
$this->renderBadge('WARN', 'Some cached tests due a re-run could not be located on disk.');
$this->renderChild('Running the full suite with replay instead of a filtered run.');
}
if ($this->filteredMode) {
$rerunFromCache = $graph->testFilesToRerun($this->branch);
}
@@ -1014,6 +1023,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
if ($this->piggybackCoverage) {
$recorder->activateLinkTracking();
$this->recordingActive = true;
$this->output->writeln('');
@@ -1235,6 +1245,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$data = $this->readPartial($key);
if ($data === null) {
$this->state->delete($key);
continue;
}
@@ -1368,6 +1380,26 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$this->saveGraph($graph);
}
/**
* Union of two per-test edge maps — piggybacked line-coverage edges plus
* the recorder's link-tracked edges (rendered Blade views, ...), which
* never appear in line coverage.
*
* @param array<string, array<int, string>> $coverage
* @param array<string, array<int, string>> $linked
* @return array<string, array<int, string>>
*/
private function mergePerTestFiles(array $coverage, array $linked): array
{
foreach ($linked as $testFile => $sources) {
$existing = $coverage[$testFile] ?? [];
$coverage[$testFile] = array_values(array_unique([...$existing, ...$sources]));
}
return $coverage;
}
private function seedResultsInto(Graph $graph): void
{
/** @var ResultCollector $collector */
@@ -1379,6 +1411,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
foreach ($results as $testId => $result) {
$file = $result['file'] ?? null;
if ($file === null || str_contains($file, "eval()'d")) {
$file = $this->resolveFailedTestFile($testId);
}
if (is_string($file) && $file !== '') {
$touchedFiles[$file] = true;
}
@@ -1394,12 +1430,13 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
);
}
$graph->markKnownTestFiles(array_keys($touchedFiles));
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$collector->reset();
}
private function snapshotTestResults(): void
private function snapshotTestResults(bool $markKnownTestFiles = false): void
{
/** @var ResultCollector $collector */
$collector = Container::getInstance()->get(ResultCollector::class);
@@ -1442,6 +1479,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
);
}
if ($markKnownTestFiles) {
$graph->markKnownTestFiles(array_keys($touchedFiles));
}
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$this->saveGraph($graph);
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Tia\Contracts;
/**
* @internal
*/
interface Lockfile
{
public function applies(string $filename): bool;
public function fingerprint(string $contents): ?string;
}
+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
+4
View File
@@ -89,6 +89,10 @@ final class FileState implements State
$keys = [];
foreach ($matches as $path) {
if (str_ends_with($path, '.tmp')) {
continue;
}
$keys[] = basename($path);
}
+45 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Plugins\Tia;
use Pest\Plugins\Tia\Contracts\Lockfile;
use Symfony\Component\Finder\Finder;
/**
@@ -11,7 +12,14 @@ use Symfony\Component\Finder\Finder;
*/
final readonly class Fingerprint
{
private const int SCHEMA_VERSION = 17;
private const int SCHEMA_VERSION = 18;
/**
* @var array<int, class-string<Lockfile>>
*/
private const array LOCKFILES = [
Lockfiles\PackageLock::class,
];
/**
* @return array{
@@ -205,7 +213,11 @@ final readonly class Fingerprint
$parts = [];
foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) {
$hash = self::trackedHash($projectRoot, $name);
if (! self::isTrackedByGit($projectRoot, $name)) {
continue;
}
$hash = self::lockfileHash($projectRoot.'/'.$name, $name);
if ($hash !== null) {
$parts[] = $name.':'.$hash;
@@ -215,6 +227,37 @@ final readonly class Fingerprint
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
}
private static function lockfileHash(string $path, string $name): ?string
{
if (! is_file($path)) {
return null;
}
$contents = @file_get_contents($path);
if ($contents === false) {
return null;
}
foreach (self::LOCKFILES as $class) {
$handler = new $class;
if (! $handler->applies($name)) {
continue;
}
$fingerprint = $handler->fingerprint($contents);
if ($fingerprint !== null) {
return $fingerprint;
}
break;
}
return hash('xxh128', $contents);
}
private static function trackedHash(string $projectRoot, string $relativePath): ?string
{
if (! self::isTrackedByGit($projectRoot, $relativePath)) {
+66 -4
View File
@@ -149,6 +149,13 @@ final class Graph
*/
private function applyMigrationChanges(array $migrationPaths, array &$affectedSet): array
{
// With no recorded table usage at all, table intersection can never
// select anything — route every migration change through the
// watch-pattern fallback instead of silently skipping tests.
if ($this->testTables === []) {
return $migrationPaths;
}
$changedTables = [];
$unparseable = [];
@@ -446,13 +453,14 @@ final class Graph
$bladeAffected = $this->affectedByStaticBladeUsage($rel);
// Only a walk that actually selected tests counts as handled — a
// component whose usage the static walk missed must still reach
// the watch-pattern fallback instead of being silently swallowed.
if ($bladeAffected !== []) {
foreach ($bladeAffected as $testFile) {
$affectedSet[$testFile] = true;
}
$staticallyHandled[$rel] = true;
} elseif ($this->isBladeComponentPath($rel)) {
$staticallyHandled[$rel] = true;
}
}
@@ -690,8 +698,16 @@ final class Graph
private function shouldRerun(int $status): bool
{
$testStatus = TestStatus::from($status);
return $this->shouldRerunStatus(TestStatus::from($status));
}
/**
* Whether a cached result with this status must be re-executed rather
* than replayed, honouring the configured failOn* / displayDetailsOn*
* policies.
*/
public function shouldRerunStatus(TestStatus $testStatus): bool
{
if ($testStatus->isFailure() || $testStatus->isError()) {
return true;
}
@@ -813,6 +829,38 @@ final class Graph
}
}
/**
* Mark test files that executed under a recorded coverage session as "known",
* seeding an empty edge set for any that produced zero project-source edges.
*
* Without this, a test that covers no application source (e.g. a pure unit
* test asserting on language primitives) never becomes an edge key, so
* {@see self::knowsTest()} reports it as unknown and it re-runs on every TIA
* run. Recording it with an empty edge set lets it be replayed/skipped while
* unchanged; it is still re-run whenever its own file changes, via
* {@see self::applyTestFileChanges()}.
*
* Must only be called from the recording path, where coverage was actually
* collected — otherwise a missing edge set could mean "coverage was off",
* not "genuinely covered nothing".
*
* @param array<int, string> $testFiles Absolute or project-relative test file paths.
*/
public function markKnownTestFiles(array $testFiles): void
{
foreach ($testFiles as $testFile) {
$rel = $this->relative($testFile);
if ($rel === null) {
continue;
}
if (! isset($this->edges[$rel])) {
$this->edges[$rel] = [];
}
}
}
/**
* @param array<string, array<int, string>> $testToTables
*/
@@ -1217,7 +1265,21 @@ final class Graph
$tail = substr($tail, 0, -strlen('.blade.php'));
$name = str_replace('/', '.', $tail);
return $name === '' ? [] : [$name, str_replace('_', '-', $name)];
if ($name === '') {
return [];
}
$names = [$name, str_replace('_', '-', $name)];
// Anonymous index components: components/card/index.blade.php resolves as <x-card>.
if (str_ends_with($name, '.index') && $name !== '.index') {
$base = substr($name, 0, -strlen('.index'));
$names[] = $base;
$names[] = str_replace('_', '-', $base);
}
return array_values(array_unique($names));
}
/** @return list<string> */
+3 -1
View File
@@ -163,7 +163,9 @@ final class JsModuleGraph
return null;
}
if (! is_dir($projectRoot.DIRECTORY_SEPARATOR.'node_modules'.DIRECTORY_SEPARATOR.'vite')) {
$nodeModules = $projectRoot.DIRECTORY_SEPARATOR.'node_modules';
if (! is_dir($nodeModules.DIRECTORY_SEPARATOR.'vite') && ! is_dir($nodeModules.DIRECTORY_SEPARATOR.'rolldown')) {
return null;
}
+79
View File
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Tia\Lockfiles;
use Pest\Plugins\Tia\Contracts\Lockfile;
/**
* @internal
*/
final readonly class PackageLock implements Lockfile
{
public function applies(string $filename): bool
{
return $filename === 'package-lock.json';
}
public function fingerprint(string $contents): ?string
{
$data = json_decode($contents, true);
if (! is_array($data) || ! isset($data['packages']) || ! is_array($data['packages'])) {
return null;
}
$packages = $data['packages'];
$platformPaths = [];
foreach ($packages as $path => $meta) {
if (is_string($path) && is_array($meta) && $this->isPlatformSpecific($meta)) {
$platformPaths[] = $path;
}
}
$entries = [];
foreach ($packages as $path => $meta) {
if (! is_string($path)) {
continue;
}
if (! is_array($meta)) {
continue;
}
if ($this->isPlatformSpecific($meta)) {
continue;
}
if ($this->isBundledUnderPlatform($path, $platformPaths)) {
continue;
}
$version = $meta['version'] ?? null;
$entries[$path] = is_string($version) ? $version : '';
}
ksort($entries);
$encoded = json_encode($entries, JSON_UNESCAPED_SLASHES);
return $encoded === false ? null : hash('xxh128', $encoded);
}
/**
* @param array<string, mixed> $meta
*/
private function isPlatformSpecific(array $meta): bool
{
return isset($meta['os']) || isset($meta['cpu']) || isset($meta['libc']);
}
/**
* @param list<string> $platformPaths
*/
private function isBundledUnderPlatform(string $path, array $platformPaths): bool
{
return array_any($platformPaths, fn (string $platformPath): bool => str_starts_with($path, $platformPath.'/node_modules/'));
}
}
+39 -3
View File
@@ -34,6 +34,8 @@ final class Recorder
private bool $active = false;
private bool $captureCoverage = false;
private bool $driverChecked = false;
private bool $driverAvailable = false;
@@ -43,6 +45,17 @@ final class Recorder
private ?SourceScope $sourceScope = null;
public function activate(): void
{
$this->active = true;
$this->captureCoverage = true;
}
/**
* Enable per-test link tracking (tables, Inertia components, database
* usage, rendered views) without driving pcov/xdebug — for runs where
* coverage edges are piggybacked from an existing PHPUnit coverage session.
*/
public function activateLinkTracking(): void
{
$this->active = true;
}
@@ -75,7 +88,11 @@ final class Recorder
public function beginTest(string $className, string $methodName, string $fallbackFile): void
{
if (! $this->active || ! $this->driverAvailable()) {
if (! $this->active) {
return;
}
if ($this->captureCoverage && ! $this->driverAvailable()) {
return;
}
@@ -95,6 +112,10 @@ final class Recorder
$this->perTestUsesDatabase[$file] = true;
}
if (! $this->captureCoverage) {
return;
}
if ($this->driver === 'pcov') {
\pcov\clear();
\pcov\start();
@@ -107,7 +128,13 @@ final class Recorder
public function endTest(): void
{
if (! $this->active || ! $this->driverAvailable() || $this->currentTestFile === null) {
if (! $this->active || $this->currentTestFile === null) {
return;
}
if (! $this->captureCoverage || ! $this->driverAvailable()) {
$this->currentTestFile = null;
return;
}
@@ -132,7 +159,15 @@ final class Recorder
$data = \xdebug_get_code_coverage();
\xdebug_stop_code_coverage(true);
$coveredFiles = array_keys($data);
$scope = $this->sourceScope();
foreach (array_keys($data) as $file) {
if (! $scope->contains($file)) {
unset($data[$file]);
}
}
$coveredFiles = $this->filesWithExecutedLines($data);
}
foreach ($coveredFiles as $sourceFile) {
@@ -351,5 +386,6 @@ final class Recorder
$this->classUsesDatabaseCache = [];
$this->sourceScope = null;
$this->active = false;
$this->captureCoverage = false;
}
}
+68 -34
View File
@@ -9,7 +9,12 @@ namespace Pest\Plugins\Tia;
*/
final class TableExtractor
{
private const array DML_PREFIXES = ['select', 'insert', 'update', 'delete'];
private const array DML_PREFIXES = ['select', 'insert', 'update', 'delete', 'with', 'replace'];
/**
* A single (optionally quoted) identifier segment.
*/
private const string IDENTIFIER = '(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|\w+)';
/**
* @return list<string> Sorted, deduped table names referenced by the
@@ -22,14 +27,15 @@ final class TableExtractor
return [];
}
$prefix = strtolower(substr($trimmed, 0, 6));
$matched = array_any(self::DML_PREFIXES, fn (string $dml): bool => str_starts_with($prefix, $dml));
if (! $matched) {
if (preg_match('/^[a-zA-Z]+/', $trimmed, $prefixMatch) !== 1) {
return [];
}
$pattern = '/(?:\bfrom|\binto|\bupdate|\bjoin)\s+(?:"([^"]+)"|`([^`]+)`|\[([^\]]+)\]|(\w+))/i';
if (! in_array(strtolower($prefixMatch[0]), self::DML_PREFIXES, true)) {
return [];
}
$pattern = '/\b(?:from|into|update|join)\s+('.self::IDENTIFIER.'(?:\s*\.\s*'.self::IDENTIFIER.')*)/i';
if (preg_match_all($pattern, $sql, $matches) === false) {
return [];
@@ -37,14 +43,9 @@ final class TableExtractor
$tables = [];
for ($i = 0, $n = count($matches[0]); $i < $n; $i++) {
$name = $matches[1][$i] !== ''
? $matches[1][$i]
: ($matches[2][$i] !== ''
? $matches[2][$i]
: ($matches[3][$i] !== ''
? $matches[3][$i]
: $matches[4][$i]));
foreach ($matches[1] as $qualified) {
$name = self::unqualified($qualified);
if ($name === '') {
continue;
}
@@ -72,40 +73,40 @@ final class TableExtractor
if (preg_match_all($schemaPattern, $php, $matches) !== false) {
foreach ($matches[1] as $i => $primary) {
$tables[strtolower($primary)] = true;
$tables[strtolower(self::lastDottedSegment($primary))] = true;
$secondary = $matches[2][$i] ?? '';
if ($secondary !== '') {
$tables[strtolower($secondary)] = true;
$tables[strtolower(self::lastDottedSegment($secondary))] = true;
}
}
}
$ddlPattern = '/(?:CREATE|ALTER|DROP|TRUNCATE|RENAME)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?\s+["`\[]?(\w+)["`\]]?/i';
$qualified = '('.self::IDENTIFIER.'(?:\s*\.\s*'.self::IDENTIFIER.')*)';
if (preg_match_all($ddlPattern, $php, $matches) !== false) {
foreach ($matches[1] as $primary) {
$lower = strtolower($primary);
if (! self::isSchemaMeta($lower)) {
$sqlPatterns = [
'/(?:CREATE|ALTER|DROP|TRUNCATE|RENAME)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?\s+'.$qualified.'/i',
'/INSERT\s+(?:IGNORE\s+)?INTO\s+'.$qualified.'/i',
'/UPDATE\s+'.$qualified.'\s+SET\b/i',
'/DELETE\s+FROM\s+'.$qualified.'/i',
];
foreach ($sqlPatterns as $pattern) {
if (preg_match_all($pattern, $php, $matches) === false) {
continue;
}
foreach ($matches[1] as $name) {
$lower = strtolower(self::unqualified($name));
if ($lower !== '' && ! self::isSchemaMeta($lower)) {
$tables[$lower] = true;
}
}
}
$dmlPatterns = [
'/INSERT\s+(?:IGNORE\s+)?INTO\s+["`\[]?(\w+)["`\]]?/i',
'/UPDATE\s+["`\[]?(\w+)["`\]]?\s+SET\b/i',
'/DELETE\s+FROM\s+["`\[]?(\w+)["`\]]?/i',
'/DB::table\(\s*[\'"]([^\'"]+)[\'"]\s*\)/',
];
foreach ($dmlPatterns as $pattern) {
if (preg_match_all($pattern, $php, $matches) === false) {
continue;
}
if (preg_match_all('/DB::table\(\s*[\'"]([^\'"]+)[\'"]\s*\)/', $php, $matches) !== false) {
foreach ($matches[1] as $name) {
$lower = strtolower($name);
if (! self::isSchemaMeta($lower)) {
$lower = strtolower(self::lastDottedSegment($name));
if ($lower !== '' && ! self::isSchemaMeta($lower)) {
$tables[$lower] = true;
}
}
@@ -117,6 +118,39 @@ final class TableExtractor
return $out;
}
/**
* The table segment of a possibly schema-qualified identifier chain,
* e.g. `"public"."users"` or `analytics.events` yield `users` / `events`.
* Empty when any segment is schema metadata (`information_schema.tables`, ...).
*/
private static function unqualified(string $qualified): string
{
$name = '';
foreach (explode('.', $qualified) as $segment) {
$segment = trim($segment, " \t\n\r\"`[]");
if ($segment === '') {
continue;
}
if (self::isSchemaMeta($segment)) {
return '';
}
$name = $segment;
}
return $name;
}
private static function lastDottedSegment(string $name): string
{
$position = strrpos($name, '.');
return $position === false ? $name : substr($name, $position + 1);
}
private static function isSchemaMeta(string $name): bool
{
$lower = strtolower($name);
+2 -4
View File
@@ -45,9 +45,7 @@ final readonly class TestPaths
$directories[] = $rel;
}
$suffix = $directory->suffix();
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
$suffixes[] = $directory->suffix();
}
foreach ($suite->files() as $file) {
@@ -61,7 +59,7 @@ final readonly class TestPaths
if ($suffixes === []) {
foreach ($configuration->testSuffixes() as $suffix) {
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
$suffixes[] = $suffix;
}
}
} catch (Throwable) {
+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
*/
@@ -6,18 +6,18 @@
FAILED Tests\Fixtures\CollisionTest > error Exception
error
at tests/Fixtures/CollisionTest.php:4
1▕ <?php
at tests/Fixtures/CollisionTest.php:6
2▕
3▕ test('error', function () {
4▕ throw new Exception('error');
5▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
6▕
7▕ test('success', function () {
8▕ expect(true)->toBeTrue();
9▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
3▕ declare(strict_types=1);
4▕
5▕ test('error', function (): void {
6▕ throw new Exception('error');
7▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
8▕
9▕ test('success', function (): void {
10▕ expect(true)->toBeTrue();
1 tests/Fixtures/CollisionTest.php:4
1 tests/Fixtures/CollisionTest.php:6
Tests: 1 failed, 1 passed (1 assertions)
+118 -5
View File
@@ -298,8 +298,8 @@
✓ it uses correct parent class
DEPR Tests\Features\Deprecated
! deprecated → str_contains(): Passing null to parameter #1 ($haystack) of type string is deprecated // tests/Features/Deprecated.php:4
! user deprecated → Since foo 1.0: This is a deprecation description // tests/Features/Deprecated.php:10
deprecated
! user deprecated → Since foo 1.0: This is a deprecation description // tests/Features/Deprecated.php:12
PASS Tests\Features\Describe - 5 todos
✓ before each
@@ -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
@@ -1279,8 +1329,8 @@
// This is another runtime note
NOTI Tests\Features\Notices
! notice → This is a notice description // tests/Features/Notices.php:4
! a "describe" group of tests → notice → This is a notice description // tests/Features/Notices.php:11
! notice → This is a notice description // tests/Features/Notices.php:6
! a "describe" group of tests → notice → This is a notice description // tests/Features/Notices.php:13
PASS Tests\Features\Pr
✓ it may be associated with an pr #1, #2
@@ -1821,10 +1871,65 @@
✓ output format → it returns a 32-character hex xxh128 hash
✓ output format → it returns a stable hash for empty content
PASS Tests\Unit\Plugins\Tia\FileState
✓ keysWithPrefix() → it lists keys matching the prefix
✓ keysWithPrefix() → it ignores in-flight temporary files from concurrent writes
PASS Tests\Unit\Plugins\Tia\Graph
✓ shouldRerunStatus() → it re-runs failures and errors, replays successes
✓ applyMigrationChanges() → it selects tests whose recorded tables intersect the changed migration
✓ applyMigrationChanges() → it falls back to watch patterns when no table usage was recorded at all
✓ rerun tracking → it reruns cached failures via their file
✓ rerun tracking → it flags cached failures whose file is unknown
✓ applyBladeStaticChanges() → it maps an anonymous index component to the views that render it
✓ applyBladeStaticChanges() → it falls back to watch patterns for components with no matched usage
✓ markKnownTestFiles() → it makes a test file with no edges known
✓ markKnownTestFiles() → it does not clobber edges of an already-known test file
✓ markKnownTestFiles() → it ignores paths outside the project root
PASS Tests\Unit\Plugins\Tia\IsEnabledForRun
✓ does not throw when an integer --random-order-seed is passed as a separate argv element
✓ still detects --tia when an integer argument is present
PASS Tests\Unit\Plugins\Tia\Lockfiles\PackageLock
✓ it applies only to package-lock.json
✓ it returns null for contents that are not an npm lockfile
✓ it is stable when the directory-derived top-level name changes
✓ it ignores platform-specific native binaries added or dropped per OS
✓ it ignores libc-constrained variants
✓ it ignores runtime deps bundled beneath a platform-specific package
✓ it detects a real dependency version change
✓ it detects an added or removed non-platform dependency
✓ it is independent of package ordering
PASS Tests\Unit\Plugins\Tia\Recorder
✓ activateLinkTracking() → it tracks tables without a coverage driver
✓ activateLinkTracking() → it tracks inertia components without a coverage driver
✓ activateLinkTracking() → it tracks linked sources across consecutive tests
✓ activateLinkTracking() → it records nothing while inactive
PASS Tests\Unit\Plugins\Tia\TableExtractor
✓ fromSql() → it extracts tables from plain DML
✓ fromSql() → it extracts tables from joins
✓ fromSql() → it extracts tables from CTE queries
✓ fromSql() → it extracts tables from REPLACE INTO
✓ fromSql() → it records the table, not the schema, for qualified identifiers
✓ fromSql() → it handles quoted identifiers
✓ fromSql() → it ignores schema metadata tables
✓ fromSql() → it returns nothing for non-DML statements
✓ fromMigrationSource() → it extracts tables from Schema builder calls
✓ fromMigrationSource() → it extracts tables from raw DDL statements
✓ fromMigrationSource() → it records the table, not the schema, in qualified DDL and DML
✓ fromMigrationSource() → it extracts tables from DB::table calls
PASS Tests\Unit\Plugins\Tia\TestPaths
✓ isTestFile() → it matches a standard test file under a configured directory
✓ isTestFile() → it does not match a non-test file that shares the directory
✓ isTestFile() → it does not match files outside the configured directories
✓ isTestFile() → it matches an explicitly listed file
✓ isTestFile() → it honours a bare .php suffix
✓ isTestFile() → it does not require a dot before the suffix
PASS Tests\Unit\Plugins\Tia\ViteDepsHelper
✓ it strips JSONC down to something JSON.parse accepts with ('plain-object')
✓ it strips JSONC down to something JSON.parse accepts with ('plain-array')
@@ -1886,6 +1991,14 @@
✓ it builds the expected alias map from a tsconfig with ('malformed-json')
✓ it builds the expected alias map from a tsconfig with ('missing-config')
✓ it builds the expected alias map from a tsconfig with ('tsconfig-precedence')
✓ it builds the expected alias map from a vite config with ('root-slash-literal')
✓ it builds the expected alias map from a vite config with ('path-resolve')
✓ it builds the expected alias map from a vite config with ('file-url')
✓ it builds the expected alias map from a vite config with ('tilde-alias')
✓ it builds the expected alias map from a vite config with ('multiple-aliases')
✓ it builds the expected alias map from a vite config with ('laravel-plugin-default')
✓ it builds the expected alias map from a vite config with ('no-alias-no-plugin')
✓ it builds the expected alias map from a vite config with ('no-config')
PASS Tests\Unit\Preset
✓ preset invalid name
@@ -2073,4 +2186,4 @@
✓ pass with dataset with ('my-datas-set-value')
✓ within describe → pass with dataset with ('my-datas-set-value')
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1452 passed (3209 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;
+5 -3
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,14 +16,14 @@ trait SecondPluginTrait
{
public function assertSecondPluginTraitGotRegistered(): void
{
$this->assertTrue(true);
expect(true)->toBeTrue();
}
}
Plugin::uses(PluginTrait::class);
Plugin::uses(SecondPluginTrait::class);
function _assertThat()
function _assertThat(): void
{
expect(true)->toBeTrue();
}
+6 -12
View File
@@ -1,19 +1,13 @@
<?php
declare(strict_types=1);
dataset('bound.closure', function () {
yield function () {
return 1;
};
yield function () {
return 2;
};
yield fn (): int => 1;
yield fn (): int => 2;
});
dataset('bound.array', [
function () {
return 1;
},
function () {
return 2;
},
fn (): int => 1,
fn (): int => 2,
]);
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
dataset('numbers.closure', function () {
yield [1];
yield [2];
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
test('global functions are loaded', function () {
declare(strict_types=1);
test('global functions are loaded', function (): void {
expect(helper_returns_string())->toBeString();
});
+25 -25
View File
@@ -1,10 +1,10 @@
<?php
beforeEach(function () {
beforeEach(function (): void {
$this->count = 0;
});
afterEach(function () {
afterEach(function (): void {
match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(1),
'__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(1),
@@ -21,37 +21,37 @@ afterEach(function () {
$this->count++;
});
it('can run after test', function () {
it('can run after test', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(2);
$this->count++;
});
it('can run after test twice', function () {
it('can run after test twice', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(2);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(3);
$this->count++;
});
it('does not run when skipped', function () {
it('does not run when skipped', function (): void {
dd('This should not run 1');
})->skip()->after(function () {
})->skip()->after(function (): void {
dd('This should not run 2');
});
afterEach(function () {
afterEach(function (): void {
match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(3),
'__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(4),
@@ -70,7 +70,7 @@ afterEach(function () {
$this->count++;
});
afterEach(function () {
afterEach(function (): void {
match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(4),
'__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(5),
@@ -87,47 +87,47 @@ afterEach(function () {
$this->count++;
});
describe('something', function () {
it('does not run when skipped', function () {
describe('something', function (): void {
it('does not run when skipped', function (): void {
dd('This should not run 3');
})->skip()->after(function () {
})->skip()->after(function (): void {
dd('This should not run 4');
});
it('can run after test', function () {
it('can run after test', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(5);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(6);
$this->count++;
});
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(4);
$this->count++;
});
describe('something 2', function () {
it('can run after test', function () {
describe('something 2', function (): void {
it('can run after test', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(6);
$this->count++;
});
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(4);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(5);
$this->count++;
@@ -136,7 +136,7 @@ describe('something 2', function () {
test('high order test')
->defer(fn () => $this->count++)
->expect(fn () => $this->count)->toBe(1)
->after(function () {
->after(function (): void {
expect($this->count)->toBe(4);
$this->count++;
@@ -146,7 +146,7 @@ test('high order test with skip')
->skip()
->defer(fn () => $this->count++)
->expect(fn () => $this->count)->toBe(1)
->after(function () {
->after(function (): void {
dd('This should not run 5');
});
+7 -5
View File
@@ -1,19 +1,21 @@
<?php
declare(strict_types=1);
$file = __DIR__.DIRECTORY_SEPARATOR.'after-all-test';
beforeAll(function () use ($file) {
beforeAll(function () use ($file): void {
@unlink($file);
});
afterAll(function () use ($file) {
afterAll(function () use ($file): void {
@unlink($file);
});
test('deletes file after all', function () use ($file) {
test('deletes file after all', function () use ($file): void {
file_put_contents($file, 'foo');
$this->assertFileExists($file);
register_shutdown_function(function () {
expect($file)->toBeFile();
register_shutdown_function(function (): void {
// $this->assertFileDoesNotExist($file);
});
});
+34 -34
View File
@@ -2,99 +2,99 @@
$state = new stdClass;
beforeEach(function () use ($state) {
beforeEach(function () use ($state): void {
$this->state = $state;
});
afterEach(function () {
afterEach(function (): void {
$this->state->bar = 1;
});
afterEach(function () {
afterEach(function (): void {
unset($this->state->bar);
});
it('does not get executed before the test', function () {
it('does not get executed before the test', function (): void {
expect($this->state)->not->toHaveProperty('bar');
});
it('gets executed after the test', function () {
expect($this->state)->toHaveProperty('bar');
expect($this->state->bar)->toBe(2);
it('gets executed after the test', function (): void {
expect($this->state)->toHaveProperty('bar')
->and($this->state->bar)->toBe(2);
});
afterEach(function () {
afterEach(function (): void {
$this->state->bar = 2;
});
describe('outer', function () {
afterEach(function () {
describe('outer', function (): void {
afterEach(function (): void {
$this->state->bar++;
});
describe('inner', function () {
afterEach(function () {
describe('inner', function (): void {
afterEach(function (): void {
$this->state->bar++;
});
it('does not get executed before the test', function () {
expect($this->state)->toHaveProperty('bar');
expect($this->state->bar)->toBe(2);
it('does not get executed before the test', function (): void {
expect($this->state)->toHaveProperty('bar')
->and($this->state->bar)->toBe(2);
});
it('should call all parent afterEach functions', function () {
expect($this->state)->toHaveProperty('bar');
expect($this->state->bar)->toBe(4);
it('should call all parent afterEach functions', function (): void {
expect($this->state)->toHaveProperty('bar')
->and($this->state->bar)->toBe(4);
});
});
});
describe('matching describe block names', function () {
afterEach(function () {
describe('matching describe block names', function (): void {
afterEach(function (): void {
$this->state->foo = 1;
});
describe('outer', function () {
afterEach(function () {
describe('outer', function (): void {
afterEach(function (): void {
$this->state->foo++;
});
describe('middle', function () {
afterEach(function () {
describe('middle', function (): void {
afterEach(function (): void {
$this->state->foo++;
});
describe('inner', function () {
afterEach(function () {
describe('inner', function (): void {
afterEach(function (): void {
$this->state->foo++;
});
it('does not get executed before the test', function () {
it('does not get executed before the test', function (): void {
expect($this)->not->toHaveProperty('foo');
});
it('should call all parent afterEach functions', function () {
it('should call all parent afterEach functions', function (): void {
expect($this->state->foo)->toBe(4);
});
});
});
describe('middle', function () {
it('does not get executed before the test', function () {
describe('middle', function (): void {
it('does not get executed before the test', function (): void {
expect($this)->not->toHaveProperty('foo');
});
it('should not call afterEach functions for sibling describe blocks with the same name', function () {
it('should not call afterEach functions for sibling describe blocks with the same name', function (): void {
expect($this)->not->toHaveProperty('foo');
});
});
describe('inner', function () {
it('does not get executed before the test', function () {
describe('inner', function (): void {
it('does not get executed before the test', function (): void {
expect($this)->not->toHaveProperty('foo');
});
it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function () {
it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function (): void {
expect($this)->not->toHaveProperty('foo');
});
});
+4 -4
View File
@@ -1,15 +1,15 @@
<?php
beforeEach(function () {
beforeEach(function (): void {
expect(true)->toBeTrue();
})->assignee('nunomaduro');
it('may be associated with an assignee', function () {
it('may be associated with an assignee', function (): void {
expect(true)->toBeTrue();
})->assignee('taylorotwell');
describe('nested', function () {
it('may be associated with an assignee', function () {
describe('nested', function (): void {
it('may be associated with an assignee', function (): void {
expect(true)->toBeTrue();
})->assignee('taylorotwell');
})->assignee('nunomaduro')->note('an note between an the assignee')->assignee(['jamesbrooks', 'joedixon']);
+3 -3
View File
@@ -3,16 +3,16 @@
$foo = new stdClass;
$foo->bar = 0;
beforeAll(function () use ($foo) {
beforeAll(function () use ($foo): void {
$foo->bar++;
});
it('gets executed before tests', function () use ($foo) {
it('gets executed before tests', function () use ($foo): void {
expect($foo)->bar->toBe(1);
$foo->bar = 'changed';
});
it('do not get executed before each test', function () use ($foo) {
it('do not get executed before each test', function () use ($foo): void {
expect($foo)->bar->toBe('changed');
});
+38 -38
View File
@@ -1,91 +1,91 @@
<?php
beforeEach(function () {
beforeEach(function (): void {
$this->bar = 2;
});
beforeEach(function () {
beforeEach(function (): void {
$this->bar++;
});
beforeEach(function () {
beforeEach(function (): void {
$this->bar = 0;
});
it('gets executed before each test', function () {
it('gets executed before each test', function (): void {
expect($this->bar)->toBe(1);
$this->bar = 'changed';
});
it('gets executed before each test once again', function () {
it('gets executed before each test once again', function (): void {
expect($this->bar)->toBe(1);
});
beforeEach(function () {
beforeEach(function (): void {
$this->bar++;
});
describe('outer', function () {
beforeEach(function () {
describe('outer', function (): void {
beforeEach(function (): void {
$this->bar++;
});
describe('inner', function () {
beforeEach(function () {
describe('inner', function (): void {
beforeEach(function (): void {
$this->bar++;
});
it('should call all parent beforeEach functions', function () {
it('should call all parent beforeEach functions', function (): void {
expect($this->bar)->toBe(3);
});
});
});
describe('with expectations', function () {
describe('with expectations', function (): void {
beforeEach()->expect(true)->toBeTrue();
describe('nested block', function () {
test('test', function () {});
describe('nested block', function (): void {
test('test', function (): void {});
});
test('test', function () {});
test('test', function (): void {});
});
describe('matching describe block names', function () {
beforeEach(function () {
describe('matching describe block names', function (): void {
beforeEach(function (): void {
$this->foo = 1;
});
describe('outer', function () {
beforeEach(function () {
describe('outer', function (): void {
beforeEach(function (): void {
$this->foo++;
});
describe('middle', function () {
beforeEach(function () {
describe('middle', function (): void {
beforeEach(function (): void {
$this->foo++;
});
describe('inner', function () {
beforeEach(function () {
describe('inner', function (): void {
beforeEach(function (): void {
$this->foo++;
});
it('should call all parent beforeEach functions', function () {
it('should call all parent beforeEach functions', function (): void {
expect($this->foo)->toBe(4);
});
});
});
describe('middle', function () {
it('should not call beforeEach functions for sibling describe blocks with the same name', function () {
describe('middle', function (): void {
it('should not call beforeEach functions for sibling describe blocks with the same name', function (): void {
expect($this->foo)->toBe(2);
});
});
describe('inner', function () {
it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function () {
describe('inner', function (): void {
it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function (): void {
expect($this->foo)->toBe(2);
});
});
@@ -93,36 +93,36 @@ describe('matching describe block names', function () {
});
$matchingNameCalls = 0;
describe('matching name', function () use (&$matchingNameCalls) {
beforeEach(function () use (&$matchingNameCalls) {
describe('matching name', function () use (&$matchingNameCalls): void {
beforeEach(function () use (&$matchingNameCalls): void {
$matchingNameCalls++;
});
it('should call the before each', function () use (&$matchingNameCalls) {
it('should call the before each', function () use (&$matchingNameCalls): void {
expect($matchingNameCalls)->toBe(1);
});
});
describe('matching name', function () use (&$matchingNameCalls) {
it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls) {
describe('matching name', function () use (&$matchingNameCalls): void {
it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls): void {
expect($matchingNameCalls)->toBe(1);
});
});
beforeEach(function () {
beforeEach(function (): void {
$this->baz = 1;
});
describe('called on all tests', function () {
beforeEach(function () {
describe('called on all tests', function (): void {
beforeEach(function (): void {
$this->baz++;
});
test('beforeEach should be called', function () {
test('beforeEach should be called', function (): void {
expect($this->baz)->toBe(2);
});
test('beforeEach should be called for all tests', function () {
test('beforeEach should be called for all tests', function (): void {
expect($this->baz)->toBe(2);
});
});
@@ -1,15 +1,17 @@
<?php
declare(strict_types=1);
beforeEach()->expect(true)->toBeTrue();
test('runs 1', function () {
test('runs 1', function (): void {
// This test did performs assertions...
});
test('runs 2', function () {
test('runs 2', function (): void {
// This test did performs assertions...
});
test('runs 3', function () {
test('runs 3', function (): void {
// This test did performs assertions...
});
@@ -1,15 +1,17 @@
<?php
declare(strict_types=1);
beforeEach()->skip();
test('does not run 1', function () {
test('does not run 1', function (): void {
$this->fail('This test should not run');
});
test('does not run 2', function () {
test('does not run 2', function (): void {
$this->fail('This test should not run');
});
test('does not run 3', function () {
test('does not run 3', function (): void {
$this->fail('This test should not run');
});
@@ -2,11 +2,11 @@
beforeEach()->todo();
test('is marked as todo 1', function () {
test('is marked as todo 1', function (): void {
$this->fail('This test should not run');
});
test('is marked as todo 2', function () {
test('is marked as todo 2', function (): void {
$this->fail('This test should not run');
});
+4 -4
View File
@@ -6,12 +6,12 @@ use Symfony\Component\Console\Output\ConsoleOutput;
it('has plugin')->assertTrue(class_exists(CoveragePlugin::class));
it('adds coverage if --coverage exist', function () {
it('adds coverage if --coverage exist', function (): void {
$plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverage)->toBeFalse();
$arguments = $plugin->handleArguments([]);
expect($arguments)->toEqual([])
expect($arguments)->toBeEmpty()
->and($plugin->coverage)->toBeFalse();
$arguments = $plugin->handleArguments(['--coverage']);
@@ -19,7 +19,7 @@ it('adds coverage if --coverage exist', function () {
->and($plugin->coverage)->toBeTrue();
})->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available');
it('adds coverage if --min exist', function () {
it('adds coverage if --min exist', function (): void {
$plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverageMin)->toEqual(0.0)
->and($plugin->coverage)->toBeFalse();
@@ -34,7 +34,7 @@ it('adds coverage if --min exist', function () {
expect($plugin->coverageMin)->toEqual(2.4);
});
it('generates coverage based on file input', function () {
it('generates coverage based on file input', function (): void {
expect(Coverage::getMissingCoverage(new class
{
public function lineCoverageData(): array
+4 -4
View File
@@ -5,9 +5,9 @@ use Tests\Fixtures\Covers\CoversClass1;
covers([CoversClass1::class]);
it('uses the correct PHPUnit attribute for class', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('uses the correct PHPUnit attribute for class', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class);
expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1');
expect($attributes[1]->getName())->toBe(CoversClass::class)
->and($attributes[1]->getArguments()[0])->toBe(CoversClass1::class);
});
+3 -1
View File
@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
use Pest\PendingCalls\TestCall;
use Pest\TestSuite;
it('throws exception if no class nor method has been found', function () {
it('throws exception if no class nor method has been found', function (): void {
$testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
$testCall->covers('fakeName');
+5 -5
View File
@@ -2,11 +2,11 @@
use PHPUnit\Framework\Attributes\CoversFunction;
function testCoversFunction() {}
function testCoversFunction(): void {}
it('uses the correct PHPUnit attribute for function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('uses the correct PHPUnit attribute for function', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(CoversFunction::class);
expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction');
expect($attributes[1]->getName())->toBe(CoversFunction::class)
->and($attributes[1]->getArguments()[0])->toBe('testCoversFunction');
})->coversFunction('testCoversFunction');
+7 -8
View File
@@ -4,14 +4,13 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction;
use Tests\Fixtures\Covers\CoversClass3;
function testCoversFunction2() {}
function testCoversFunction2(): void {}
it('guesses if the given argument is a class or function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('guesses if the given argument is a class or function', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class);
expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class);
expect($attributes[2]->getName())->toBe(CoversFunction::class);
expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2');
expect($attributes[1]->getName())->toBe(CoversClass::class)
->and($attributes[1]->getArguments()[0])->toBe(CoversClass3::class)
->and($attributes[2]->getName())->toBe(CoversFunction::class)
->and($attributes[2]->getArguments()[0])->toBe('testCoversFunction2');
})->covers(CoversClass3::class, 'testCoversFunction2');
+4 -4
View File
@@ -3,9 +3,9 @@
use PHPUnit\Framework\Attributes\CoversTrait as PHPUnitCoversTrait;
use Tests\Fixtures\Covers\CoversTrait;
it('uses the correct PHPUnit attribute for trait', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('uses the correct PHPUnit attribute for trait', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class);
expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait');
expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class)
->and($attributes[1]->getArguments()[0])->toBe(CoversTrait::class);
})->coversTrait(CoversTrait::class);
+78 -78
View File
@@ -11,41 +11,41 @@
// beforeEach()->with() inside describe blocks
// ---------------------------------------------------------------
describe('beforeEach()->with() applies dataset to tests', function () {
describe('beforeEach()->with() applies dataset to tests', function (): void {
beforeEach()->with([10]);
test('receives the dataset value', function ($value) {
test('receives the dataset value', function ($value): void {
expect($value)->toBe(10);
});
it('also receives the dataset value in it()', function ($value) {
it('also receives the dataset value in it()', function ($value): void {
expect($value)->toBe(10);
});
});
describe('beforeEach()->with() with multiple dataset values', function () {
describe('beforeEach()->with() with multiple dataset values', function (): void {
beforeEach()->with([1, 2, 3]);
test('receives each value from the dataset', function ($value) {
test('receives each value from the dataset', function ($value): void {
expect($value)->toBeIn([1, 2, 3]);
});
});
describe('beforeEach()->with() with keyed dataset', function () {
describe('beforeEach()->with() with keyed dataset', function (): void {
beforeEach()->with(['first' => [10], 'second' => [20]]);
test('receives keyed dataset values', function ($value) {
test('receives keyed dataset values', function ($value): void {
expect($value)->toBeIn([10, 20]);
});
});
describe('beforeEach()->with() with closure dataset', function () {
describe('beforeEach()->with() with closure dataset', function (): void {
beforeEach()->with(function () {
yield [100];
yield [200];
});
test('receives values from closure dataset', function ($value) {
test('receives values from closure dataset', function ($value): void {
expect($value)->toBeIn([100, 200]);
});
});
@@ -54,30 +54,30 @@ describe('beforeEach()->with() with closure dataset', function () {
// describe()->with() method chaining
// ---------------------------------------------------------------
describe('describe()->with() passes dataset to tests', function () {
test('receives the dataset value', function ($value) {
describe('describe()->with() passes dataset to tests', function (): void {
test('receives the dataset value', function ($value): void {
expect($value)->toBe(42);
});
it('also receives it in it()', function ($value) {
it('also receives it in it()', function ($value): void {
expect($value)->toBe(42);
});
})->with([42]);
describe('describe()->with() with multiple values', function () {
test('receives each value', function ($value) {
describe('describe()->with() with multiple values', function (): void {
test('receives each value', function ($value): void {
expect($value)->toBeIn([5, 10, 15]);
});
})->with([5, 10, 15]);
describe('describe()->with() with keyed dataset', function () {
test('receives keyed values', function ($value) {
describe('describe()->with() with keyed dataset', function (): void {
test('receives keyed values', function ($value): void {
expect($value)->toBeIn([100, 200]);
});
})->with(['alpha' => [100], 'beta' => [200]]);
describe('describe()->with() with closure dataset', function () {
test('receives closure dataset values', function ($value) {
describe('describe()->with() with closure dataset', function (): void {
test('receives closure dataset values', function ($value): void {
expect($value)->toBeIn([7, 14]);
});
})->with(function () {
@@ -89,33 +89,33 @@ describe('describe()->with() with closure dataset', function () {
// Nested describe blocks with datasets
// ---------------------------------------------------------------
describe('outer with dataset', function () {
describe('inner without dataset', function () {
test('inherits outer dataset', function (...$args) {
describe('outer with dataset', function (): void {
describe('inner without dataset', function (): void {
test('inherits outer dataset', function (...$args): void {
expect($args)->toBe([1]);
});
});
})->with([1]);
describe('nested describe blocks with datasets at multiple levels', function () {
describe('level 1', function () {
test('receives level 1 dataset', function (...$args) {
describe('nested describe blocks with datasets at multiple levels', function (): void {
describe('level 1', function (): void {
test('receives level 1 dataset', function (...$args): void {
expect($args)->toBe([10]);
});
describe('level 2', function () {
test('receives datasets from all ancestor levels', function (...$args) {
describe('level 2', function (): void {
test('receives datasets from all ancestor levels', function (...$args): void {
expect($args)->toBe([10, 20]);
});
})->with([20]);
})->with([10]);
});
describe('deeply nested describe with datasets', function () {
describe('a', function () {
describe('b', function () {
describe('c', function () {
test('receives all ancestor datasets', function (...$args) {
describe('deeply nested describe with datasets', function (): void {
describe('a', function (): void {
describe('b', function (): void {
describe('c', function (): void {
test('receives all ancestor datasets', function (...$args): void {
expect($args)->toBe([1, 2, 3]);
});
})->with([3]);
@@ -127,19 +127,19 @@ describe('deeply nested describe with datasets', function () {
// Combining hook datasets with test-level datasets
// ---------------------------------------------------------------
describe('beforeEach()->with() combined with test->with()', function () {
describe('beforeEach()->with() combined with test->with()', function (): void {
beforeEach()->with([10]);
test('receives both datasets as cross product', function ($hookValue, $testValue) {
expect($hookValue)->toBe(10);
expect($testValue)->toBeIn([1, 2]);
test('receives both datasets as cross product', function ($hookValue, $testValue): void {
expect($hookValue)->toBe(10)
->and($testValue)->toBeIn([1, 2]);
})->with([1, 2]);
});
describe('describe()->with() combined with test->with()', function () {
test('receives both datasets', function ($describeValue, $testValue) {
expect($describeValue)->toBe(5);
expect($testValue)->toBeIn([50, 60]);
describe('describe()->with() combined with test->with()', function (): void {
test('receives both datasets', function ($describeValue, $testValue): void {
expect($describeValue)->toBe(5)
->and($testValue)->toBeIn([50, 60]);
})->with([50, 60]);
})->with([5]);
@@ -147,33 +147,33 @@ describe('describe()->with() combined with test->with()', function () {
// beforeEach()->with() combined with beforeEach closure
// ---------------------------------------------------------------
describe('beforeEach closure and beforeEach()->with() coexist', function () {
beforeEach(function () {
describe('beforeEach closure and beforeEach()->with() coexist', function (): void {
beforeEach(function (): void {
$this->setupValue = 'initialized';
});
beforeEach()->with([99]);
test('has both the closure state and dataset', function ($value) {
expect($this->setupValue)->toBe('initialized');
expect($value)->toBe(99);
test('has both the closure state and dataset', function ($value): void {
expect($this->setupValue)->toBe('initialized')
->and($value)->toBe(99);
});
});
describe('beforeEach()->with() does not interfere with closure hooks', function () {
beforeEach(function () {
describe('beforeEach()->with() does not interfere with closure hooks', function (): void {
beforeEach(function (): void {
$this->counter = 1;
});
beforeEach(function () {
beforeEach(function (): void {
$this->counter++;
});
beforeEach()->with([42]);
test('closures run in order and dataset is applied', function ($value) {
expect($this->counter)->toBe(2);
expect($value)->toBe(42);
test('closures run in order and dataset is applied', function ($value): void {
expect($this->counter)->toBe(2)
->and($value)->toBe(42);
});
});
@@ -181,24 +181,24 @@ describe('beforeEach()->with() does not interfere with closure hooks', function
// Dataset isolation between describe blocks
// ---------------------------------------------------------------
describe('first describe with dataset', function () {
describe('first describe with dataset', function (): void {
beforeEach()->with([111]);
test('gets its own dataset', function ($value) {
test('gets its own dataset', function ($value): void {
expect($value)->toBe(111);
});
});
describe('second describe with different dataset', function () {
describe('second describe with different dataset', function (): void {
beforeEach()->with([222]);
test('gets its own dataset, not the sibling', function ($value) {
test('gets its own dataset, not the sibling', function ($value): void {
expect($value)->toBe(222);
});
});
describe('third describe without dataset', function () {
test('has no dataset leaking from siblings', function () {
describe('third describe without dataset', function (): void {
test('has no dataset leaking from siblings', function (): void {
expect(true)->toBeTrue();
});
});
@@ -207,23 +207,23 @@ describe('third describe without dataset', function () {
// describe()->with() combined with beforeEach hooks
// ---------------------------------------------------------------
describe('describe()->with() with beforeEach closure', function () {
beforeEach(function () {
describe('describe()->with() with beforeEach closure', function (): void {
beforeEach(function (): void {
$this->hookRan = true;
});
test('both hook and dataset work', function ($value) {
expect($this->hookRan)->toBeTrue();
expect($value)->toBe(77);
test('both hook and dataset work', function ($value): void {
expect($this->hookRan)->toBeTrue()
->and($value)->toBe(77);
});
})->with([77]);
describe('describe()->with() with afterEach closure', function () {
afterEach(function () {
describe('describe()->with() with afterEach closure', function (): void {
afterEach(function (): void {
expect($this->value)->toBe(88);
});
test('dataset is available and afterEach runs', function ($value) {
test('dataset is available and afterEach runs', function ($value): void {
$this->value = $value;
expect($value)->toBe(88);
});
@@ -233,18 +233,18 @@ describe('describe()->with() with afterEach closure', function () {
// Multiple tests in a describe with beforeEach()->with()
// ---------------------------------------------------------------
describe('multiple tests share the same beforeEach dataset', function () {
describe('multiple tests share the same beforeEach dataset', function (): void {
beforeEach()->with([33]);
test('first test gets the dataset', function ($value) {
test('first test gets the dataset', function ($value): void {
expect($value)->toBe(33);
});
test('second test also gets the dataset', function ($value) {
test('second test also gets the dataset', function ($value): void {
expect($value)->toBe(33);
});
it('third test with it() also gets the dataset', function ($value) {
it('third test with it() also gets the dataset', function ($value): void {
expect($value)->toBe(33);
});
});
@@ -253,21 +253,21 @@ describe('multiple tests share the same beforeEach dataset', function () {
// Nested describe with beforeEach()->with() at inner level
// ---------------------------------------------------------------
describe('outer describe', function () {
beforeEach(function () {
describe('outer describe', function (): void {
beforeEach(function (): void {
$this->outer = true;
});
describe('inner describe with dataset on hook', function () {
describe('inner describe with dataset on hook', function (): void {
beforeEach()->with([55]);
test('inherits outer beforeEach and has inner dataset', function ($value) {
expect($this->outer)->toBeTrue();
expect($value)->toBe(55);
test('inherits outer beforeEach and has inner dataset', function ($value): void {
expect($this->outer)->toBeTrue()
->and($value)->toBe(55);
});
});
test('outer test is unaffected by inner dataset', function () {
test('outer test is unaffected by inner dataset', function (): void {
expect($this->outer)->toBeTrue();
});
});
@@ -276,12 +276,12 @@ describe('outer describe', function () {
// describe()->with() with depends
// ---------------------------------------------------------------
describe('describe()->with() preserves depends', function () {
test('first', function ($value) {
describe('describe()->with() preserves depends', function (): void {
test('first', function ($value): void {
expect($value)->toBe(9);
});
test('second', function ($value) {
test('second', function ($value): void {
expect($value)->toBe(9);
})->depends('first');
})->with([9]);
+10 -13
View File
@@ -6,7 +6,7 @@ $run = function (string $target): array {
$process = new Process(
['php', 'bin/pest', $target],
dirname(__DIR__, 2),
['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'],
['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1'],
);
$process->run();
@@ -17,33 +17,30 @@ $run = function (string $target): array {
];
};
test('reports missing datasets as errors for a single file run', function () use ($run) {
test('reports missing datasets as errors for a single file run', function () use ($run): void {
$result = $run('tests/.tests/IssueOnly.php');
expect($result['output'])
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
->toContain('FAILED')
->toContain('Tests: 1 failed');
expect($result['code'])->not->toBe(0);
->toContain('Tests: 1 failed')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
test('reports missing datasets as errors alongside passing tests', function () use ($run) {
test('reports missing datasets as errors alongside passing tests', function () use ($run): void {
$result = $run('tests/.tests/IssueWithPassing.php');
expect($result['output'])
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
->toContain('1 passed')
->toContain('1 failed');
expect($result['code'])->not->toBe(0);
->toContain('1 failed')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
test('reports dataset closure exceptions as errors', function () use ($run) {
test('reports dataset closure exceptions as errors', function () use ($run): void {
$result = $run('tests/.tests/DatasetClosureThrows.php');
expect($result['output'])
->toContain('boom from dataset');
expect($result['code'])->not->toBe(0);
->toContain('boom from dataset')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
+142 -189
View File
@@ -5,25 +5,20 @@ use Pest\Exceptions\DatasetDoesNotExist;
use Pest\Plugin;
use Pest\Repositories\DatasetsRepository;
beforeEach(function () {
beforeEach(function (): void {
$this->foo = 'bar';
});
it('throws exception if dataset does not exist', function () {
$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__);
it('throws exception if dataset does not exist', function (): void {
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 () {
DatasetsRepository::set('second', [[]], __DIR__);
$this->expectException(DatasetAlreadyExists::class);
$this->expectExceptionMessage('A dataset with the name `second` already exists in scope ['.__DIR__.'].');
it('throws exception if dataset already exist', function (): void {
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 () {
it('sets closures', function (): void {
DatasetsRepository::set('foo', function () {
yield [1];
}, __DIR__);
@@ -31,18 +26,18 @@ it('sets closures', function () {
expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]);
});
it('sets arrays', function () {
it('sets arrays', function (): void {
DatasetsRepository::set('bar', [[2]], __DIR__);
expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]);
});
it('gets bound to test case object', function ($value) {
$this->assertTrue(true);
it('gets bound to test case object', function ($value): void {
expect(true)->toBeTrue();
})->with([['a'], ['b']]);
test('it truncates the description', function () {
expect(true)->toBe(true);
test('it truncates the description', function (): void {
expect(true)->toBeTrue();
// it gets tested by the integration test
})->with([str_repeat('Fooo', 10)]);
@@ -51,61 +46,59 @@ $state->text = '';
$datasets = [[1], [2]];
test('lazy datasets', function ($text) use ($state, $datasets) {
test('lazy datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect(in_array([$text], $datasets))->toBe(true);
expect([$text])->toBeIn($datasets);
})->with($datasets);
test('lazy datasets did the job right', function () use ($state) {
test('lazy datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12');
});
test('interpolated :dataset lazy datasets', function ($text) {
test('interpolated :dataset lazy datasets', function ($text): void {
expect(true)->toBeTrue();
})->with($datasets);
$state->text = '';
test('eager datasets', function ($text) use ($state, $datasets) {
test('eager datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with(function () use ($datasets) {
return $datasets;
});
})->with(fn (): array => $datasets);
test('eager datasets did the job right', function () use ($state) {
test('eager datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212');
});
test('lazy registered datasets', function ($text) use ($state, $datasets) {
test('lazy registered datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with('numbers.array');
test('lazy registered datasets did the job right', function () use ($state) {
test('lazy registered datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212');
});
test('eager registered datasets', function ($text) use ($state, $datasets) {
test('eager registered datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with('numbers.closure');
test('eager registered datasets did the job right', function () use ($state) {
test('eager registered datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12121212');
});
test('eager wrapped registered datasets', function ($text) use ($state, $datasets) {
test('eager wrapped registered datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with('numbers.closure.wrapped');
test('eager registered wrapped datasets did the job right', function () use ($state) {
test('eager registered wrapped datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212121212');
});
test('named datasets', function ($text) use ($state, $datasets) {
test('named datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with([
@@ -113,14 +106,14 @@ test('named datasets', function ($text) use ($state, $datasets) {
'two' => [2],
]);
test('interpolated :dataset named datasets', function ($text) {
test('interpolated :dataset named datasets', function ($text): void {
expect(true)->toBeTrue();
})->with([
'one' => [1],
'two' => [2],
]);
test('named datasets did the job right', function () use ($state) {
test('named datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212');
});
@@ -133,13 +126,13 @@ $namedDatasets = [
new Bar,
];
test('lazy named datasets', function ($text) {
test('lazy named datasets', function ($text): void {
expect(true)->toBeTrue();
})->with($namedDatasets);
$counter = 0;
it('creates unique test case names', function (string $name, Plugin $plugin, bool $bool) use (&$counter) {
it('creates unique test case names', function (string $name, Plugin $plugin, bool $bool) use (&$counter): void {
expect(true)->toBeTrue();
$counter++;
})->with([
@@ -151,73 +144,69 @@ it('creates unique test case names', function (string $name, Plugin $plugin, boo
['Name 1', new Plugin, true],
]);
it('creates unique test case names - count', function () use (&$counter) {
it('creates unique test case names - count', function () use (&$counter): void {
expect($counter)->toBe(6);
});
$datasets_a = [[1], [2]];
$datasets_b = [[3], [4]];
test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b]);
})->with($datasets_a, $datasets_b);
test('lazy multiple datasets did the job right', function () use ($state) {
test('lazy multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12121212121213142324');
});
$state->text = '';
test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
})->with(function () use ($datasets_a) {
return $datasets_a;
})->with(function () use ($datasets_b) {
return $datasets_b;
});
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b]);
})->with(fn (): array => $datasets_a)->with(fn (): array => $datasets_b);
test('eager multiple datasets did the job right', function () use ($state) {
test('eager multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212121212121314232413142324');
});
test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void {
$state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]);
expect($datasets)->toContain([$text_b]);
expect($datasets)->toContain([$text_a])
->toContain([$text_b]);
})->with('numbers.array')->with('numbers.array');
test('lazy registered multiple datasets did the job right', function () use ($state) {
test('lazy registered multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212131423241314232411122122');
});
test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void {
$state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]);
expect($datasets)->toContain([$text_b]);
expect($datasets)->toContain([$text_a])
->toContain([$text_b]);
})->with('numbers.array')->with('numbers.closure');
test('eager registered multiple datasets did the job right', function () use ($state) {
test('eager registered multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12121212121213142324131423241112212211122122');
});
test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void {
$state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]);
expect($datasets)->toContain([$text_b]);
expect($datasets)->toContain([$text_a])
->toContain([$text_b]);
})->with('numbers.closure.wrapped')->with('numbers.closure');
test('eager wrapped registered multiple datasets did the job right', function () use ($state) {
test('eager wrapped registered multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212121212121314232413142324111221221112212211122122');
});
test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b]);
})->with([
'one' => [1],
'two' => [2],
@@ -226,18 +215,18 @@ test('named multiple datasets', function ($text_a, $text_b) use ($state, $datase
'four' => [4],
]);
test('named multiple datasets did the job right', function () use ($state) {
test('named multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324');
});
test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b) {
test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b.$text_c;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
expect([5, 6])->toContain($text_c);
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b])
->and([5, 6])->toContain($text_c);
})->with($datasets_a, $datasets_b)->with([5, 6]);
test('more than two datasets did the job right', function () use ($state) {
test('more than two datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246');
});
@@ -250,125 +239,97 @@ test(
function (int $text) use (
$wrapped_generator_state,
$wrapped_generator_function_datasets
) {
): void {
$wrapped_generator_state->text .= $text;
expect(in_array($text, $wrapped_generator_function_datasets))->toBe(true);
expect($text)->toBeIn($wrapped_generator_function_datasets);
}
)->with('numbers.generators.wrapped');
test('eager registered wrapped datasets with Generator functions did the job right', function () use ($wrapped_generator_state) {
test('eager registered wrapped datasets with Generator functions did the job right', function () use ($wrapped_generator_state): void {
expect($wrapped_generator_state->text)->toBe('1234');
});
test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description) {
test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description): void {
expect($wrapped_generator_state_with_description)->not->toBeEmpty();
})->with(function () {
yield 'taylor' => 'taylor@laravel.com';
yield 'james' => 'james@laravel.com';
});
it('can resolve a dataset after the test case is available', function ($result) {
it('can resolve a dataset after the test case is available', function ($result): void {
expect($result)->toBe('bar');
})->with([
function () {
return $this->foo;
},
fn () => $this->foo,
[
function () {
return $this->foo;
},
fn () => $this->foo,
],
]);
it('can resolve a dataset after the test case is available with multiple datasets', function (string $result, string $result2) {
it('can resolve a dataset after the test case is available with multiple datasets', function (string $result, string $result2): void {
expect($result)->toBe('bar');
})->with([
function () {
return $this->foo;
},
fn () => $this->foo,
[
function () {
return $this->foo;
},
fn () => $this->foo,
],
], [
function () {
return $this->foo;
},
fn () => $this->foo,
[
function () {
return $this->foo;
},
fn () => $this->foo,
],
]);
it('can resolve a dataset after the test case is available with shared yield sets', function ($result) {
it('can resolve a dataset after the test case is available with shared yield sets', function ($result): void {
expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.closure');
it('can resolve a dataset after the test case is available with shared array sets', function ($result) {
it('can resolve a dataset after the test case is available with shared array sets', function ($result): void {
expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.array');
it('resolves a potential bound dataset logically', function ($foo, $bar) {
expect($foo)->toBe('foo');
expect($bar())->toBe('bar');
it('resolves a potential bound dataset logically', function ($foo, $bar): void {
expect($foo)->toBe('foo')
->and($bar())->toBe('bar');
})->with([
[
'foo',
function () {
return 'bar';
},
fn (): string => 'bar',
], // This should be passed as a closure because we've passed multiple arguments
]);
it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar) {
expect($foo())->toBe('foo');
expect($bar)->toBe('bar');
it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar): void {
expect($foo())->toBe('foo')
->and($bar)->toBe('bar');
})->with([
[
function () {
return 'foo';
}, 'bar',
fn (): string => 'foo', 'bar',
], // This should be passed as a closure because we've passed multiple arguments
]);
it('will not resolve a closure if it is type hinted as a closure', function (Closure $data) {
it('will not resolve a closure if it is type hinted as a closure', function (Closure $data): void {
expect($data())->toBeString();
})->with([
function () {
return 'foo';
},
function () {
return 'bar';
},
fn (): string => 'foo',
fn (): string => 'bar',
]);
it('will not resolve a closure if it is type hinted as a callable', function (callable $data) {
it('will not resolve a closure if it is type hinted as a callable', function (callable $data): void {
expect($data())->toBeString();
})->with([
function () {
return 'foo';
},
function () {
return 'bar';
},
fn (): string => 'foo',
fn (): string => 'bar',
]);
it('can correctly resolve a bound dataset that returns an array', function (array $data) {
it('can correctly resolve a bound dataset that returns an array', function (array $data): void {
expect($data)->toBe(['foo', 'bar', 'baz']);
})->with([
function () {
return ['foo', 'bar', 'baz'];
},
fn (): array => ['foo', 'bar', 'baz'],
]);
it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz) {
it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz): void {
expect([$foo, $bar, $baz])->toBe(['foo', 'bar', 'baz']);
})->with([
function () {
return ['foo', 'bar', 'baz'];
},
fn (): array => ['foo', 'bar', 'baz'],
]);
todo('forbids to define tests in Datasets dirs and Datasets.php files');
@@ -393,57 +354,57 @@ it('may be used with high order even when bound')
->expect(fn (string $greeting) => $greeting)
->throws(InvalidArgumentException::class);
describe('with on nested describe', function () {
describe('nested', function () {
test('before inner describe block', function (...$args) {
describe('with on nested describe', function (): void {
describe('nested', function (): void {
test('before inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
describe('describe', function () {
it('should include the with value from all parent describe blocks', function (...$args) {
describe('describe', function (): void {
it('should include the with value from all parent describe blocks', function (...$args): void {
expect($args)->toBe([1, 2]);
});
test('should include the with value from all parent describe blocks and the test', function (...$args) {
test('should include the with value from all parent describe blocks and the test', function (...$args): void {
expect($args)->toBe([1, 2, 3]);
})->with([3]);
})->with([2]);
test('after inner describe block', function (...$args) {
test('after inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
})->with([1]);
});
describe('matching describe block names', function () {
describe('outer', function () {
test('before inner describe block', function (...$args) {
describe('matching describe block names', function (): void {
describe('outer', function (): void {
test('before inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
describe('inner', function () {
it('should include the with value from all parent describe blocks', function (...$args) {
describe('inner', function (): void {
it('should include the with value from all parent describe blocks', function (...$args): void {
expect($args)->toBe([1, 2]);
});
test('should include the with value from all parent describe blocks and the test', function (...$args) {
test('should include the with value from all parent describe blocks and the test', function (...$args): void {
expect($args)->toBe([1, 2, 3]);
})->with([3]);
})->with([2]);
describe('inner', function () {
it('should not include the value from the other describe block with the same name', function (...$args) {
describe('inner', function (): void {
it('should not include the value from the other describe block with the same name', function (...$args): void {
expect($args)->toBe([1]);
});
});
test('after inner describe block', function (...$args) {
test('after inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
})->with([1]);
});
test('after describe block', function (...$args) {
test('after describe block', function (...$args): void {
expect($args)->toBe([5]);
})->with([5]);
@@ -454,64 +415,58 @@ it('may be used with high order after describe block')
dataset('after-describe', ['after']);
test('after describe block with named dataset', function (...$args) {
test('after describe block with named dataset', function (...$args): void {
expect($args)->toBe(['after']);
})->with('after-describe');
test('named parameters match by parameter name', function (string $email, string $name) {
expect($name)->toBe('Taylor');
expect($email)->toBe('taylor@laravel.com');
test('named parameters match by parameter name', function (string $email, string $name): void {
expect($name)->toBe('Taylor')
->and($email)->toBe('taylor@laravel.com');
})->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
]);
test('named parameters work with multiple dataset items', function (string $email, string $name) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('named parameters work with multiple dataset items', function (string $email, string $name): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
['name' => 'James', 'email' => 'james@laravel.com'],
]);
test('named parameters work in different order than closure params', function (string $third, string $first, string $second) {
expect($first)->toBe('a');
expect($second)->toBe('b');
expect($third)->toBe('c');
test('named parameters work in different order than closure params', function (string $third, string $first, string $second): void {
expect($first)->toBe('a')
->and($second)->toBe('b')
->and($third)->toBe('c');
})->with([
['first' => 'a', 'second' => 'b', 'third' => 'c'],
]);
test('named parameters work with named dataset keys', function (string $email, string $name) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('named parameters work with named dataset keys', function (string $email, string $name): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with([
'taylor' => ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
'james' => ['name' => 'James', 'email' => 'james@laravel.com'],
]);
test('named parameters work with closures that should be resolved', function (string $email, string $name) {
expect($name)->toBe('bar');
expect($email)->toBe('bar@example.com');
test('named parameters work with closures that should be resolved', function (string $email, string $name): void {
expect($name)->toBe('bar')
->and($email)->toBe('bar@example.com');
})->with([
[
'name' => function () {
return $this->foo;
},
'email' => function () {
return $this->foo.'@example.com';
},
'name' => fn () => $this->foo,
'email' => fn (): string => $this->foo.'@example.com',
],
]);
test('named parameters work with closure type hints', function (Closure $callback, string $name) {
expect($name)->toBe('Taylor');
expect($callback())->toBe('resolved');
test('named parameters work with closure type hints', function (Closure $callback, string $name): void {
expect($name)->toBe('Taylor')
->and($callback())->toBe('resolved');
})->with([
[
'name' => 'Taylor',
'callback' => function () {
return 'resolved';
},
'callback' => fn (): string => 'resolved',
],
]);
@@ -520,23 +475,21 @@ dataset('named-params-dataset', [
['name' => 'James', 'email' => 'james@laravel.com'],
]);
test('named parameters work with registered datasets', function (string $email, string $name) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('named parameters work with registered datasets', function (string $email, string $name): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with('named-params-dataset');
test('named parameters work with bound closure returning associative array', function (string $email, string $name) {
expect($name)->toBe('bar');
expect($email)->toBe('test@example.com');
test('named parameters work with bound closure returning associative array', function (string $email, string $name): void {
expect($name)->toBe('bar')
->and($email)->toBe('test@example.com');
})->with([
function () {
return ['name' => $this->foo, 'email' => 'test@example.com'];
},
fn (): array => ['name' => $this->foo, 'email' => 'test@example.com'],
]);
test('dataset items can mix named and sequential styles', function (string $name, string $email) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('dataset items can mix named and sequential styles', function (string $name, string $email): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
['James', 'james@laravel.com'],
+17 -17
View File
@@ -2,34 +2,34 @@
$runCounter = 0;
test('first', function () use (&$runCounter) {
test('first', function () use (&$runCounter): string {
expect(true)->toBeTrue();
$runCounter++;
return 'first';
});
test('second', function () use (&$runCounter) {
test('second', function () use (&$runCounter): string {
expect(true)->toBeTrue();
$runCounter++;
return 'second';
});
test('depends', function () {
test('depends', function (): void {
expect(func_get_args())->toBe(['first', 'second']);
})->depends('first', 'second');
test('depends with ...params', function (string ...$params) {
test('depends with ...params', function (string ...$params): void {
expect(func_get_args())->toBe($params);
})->depends('first', 'second');
test('depends with defined arguments', function (string $first, string $second) {
expect($first)->toBe('first');
expect($second)->toBe('second');
test('depends with defined arguments', function (string $first, string $second): void {
expect($first)->toBe('first')
->and($second)->toBe('second');
})->depends('first', 'second');
test('depends run test only once', function () use (&$runCounter) {
test('depends run test only once', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('first', 'second');
@@ -37,42 +37,42 @@ test('depends run test only once', function () use (&$runCounter) {
it('asserts true is true')->assertTrue(true);
test('depends works with the correct test name')->assertTrue(true)->depends('it asserts true is true');
describe('describe block', function () {
describe('describe block', function (): void {
$runCounter = 0;
test('first in describe', function () use (&$runCounter) {
test('first in describe', function () use (&$runCounter): void {
$runCounter++;
expect(true)->toBeTrue();
});
test('second in describe', function () use (&$runCounter) {
test('second in describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(1);
$runCounter++;
})->depends('first in describe');
test('third in describe', function () use (&$runCounter) {
test('third in describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('second in describe');
describe('nested describe', function () {
describe('nested describe', function (): void {
$runCounter = 0;
test('first in nested describe', function () use (&$runCounter) {
test('first in nested describe', function () use (&$runCounter): void {
$runCounter++;
expect(true)->toBeTrue();
});
test('second in nested describe', function () use (&$runCounter) {
test('second in nested describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(1);
$runCounter++;
})->depends('first in nested describe');
test('third in nested describe', function () use (&$runCounter) {
test('third in nested describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('second in nested describe');
});
});
test('depends on test after describe block', function () use (&$runCounter) {
test('depends on test after describe block', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('first', 'second');
+5 -5
View File
@@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase;
class InheritanceTest extends TestCase
{
public function foo()
public function foo(): string
{
return 'bar';
}
@@ -12,11 +12,11 @@ class InheritanceTest extends TestCase
pest()->extend(InheritanceTest::class);
it('is a test', function () {
it('is a test', function (): void {
expect(true)->toBeTrue();
});
it('uses correct parent class', function () {
expect(get_parent_class($this))->toEqual(InheritanceTest::class);
expect($this->foo())->toEqual('bar');
it('uses correct parent class', function (): void {
expect(get_parent_class($this))->toEqual(InheritanceTest::class)
->and($this->foo())->toEqual('bar');
})->depends('it is a test');
+5 -3
View File
@@ -1,12 +1,14 @@
<?php
test('deprecated', function () {
str_contains(null, null);
declare(strict_types=1);
test('deprecated', function (): void {
str_contains('', '');
expect(true)->toBeTrue();
});
test('user deprecated', function () {
test('user deprecated', function (): void {
trigger_error('Since foo 1.0: This is a deprecation description', \E_USER_DEPRECATED);
expect(true)->toBeTrue();
+27 -27
View File
@@ -2,58 +2,58 @@
beforeEach(fn () => $this->count = 1);
test('before each', function () {
test('before each', function (): void {
expect($this->count)->toBe(1);
});
describe('hooks', function () {
beforeEach(function () {
describe('hooks', function (): void {
beforeEach(function (): void {
$this->count++;
});
test('value', function () {
test('value', function (): void {
expect($this->count)->toBe(2);
$this->count++;
});
afterEach(function () {
afterEach(function (): void {
expect($this->count)->toBe(3);
});
});
describe('hooks in different orders', function () {
beforeEach(function () {
describe('hooks in different orders', function (): void {
beforeEach(function (): void {
$this->count++;
});
test('value', function () {
test('value', function (): void {
expect($this->count)->toBe(3);
$this->count++;
});
afterEach(function () {
afterEach(function (): void {
expect($this->count)->toBe(4);
});
beforeEach(function () {
beforeEach(function (): void {
$this->count++;
});
});
test('todo')->todo()->shouldNotRun();
test('previous describable before each does not get applied here', function () {
test('previous describable before each does not get applied here', function (): void {
expect($this->count)->toBe(1);
});
describe('todo on hook', function () {
describe('todo on hook', function (): void {
beforeEach()->todo();
test('should not fail')->shouldNotRun();
test('should run')->expect(true)->toBeTrue();
});
describe('todo on describe', function () {
describe('todo on describe', function (): void {
test('should not fail')->shouldNotRun();
test('should run')->expect(true)->toBeTrue();
@@ -63,48 +63,48 @@ test('should run')->expect(true)->toBeTrue();
test('with', fn ($foo) => expect($foo)->toBe(1))->with([1]);
describe('with on hook', function () {
describe('with on hook', function (): void {
beforeEach()->with([2]);
test('value', function ($foo) {
test('value', function ($foo): void {
expect($foo)->toBe(2);
});
});
describe('with on describe', function () {
test('value', function ($foo) {
describe('with on describe', function (): void {
test('value', function ($foo): void {
expect($foo)->toBe(3);
});
})->with([3]);
describe('depends on describe', function () {
test('foo', function () {
describe('depends on describe', function (): void {
test('foo', function (): void {
expect('foo')->toBe('foo');
});
test('bar', function () {
test('bar', function (): void {
expect('bar')->toBe('bar');
})->depends('foo');
});
describe('depends on describe using with', function () {
test('foo', function ($foo) {
describe('depends on describe using with', function (): void {
test('foo', function ($foo): void {
expect($foo)->toBe(3);
});
test('bar', function ($foo) {
test('bar', function ($foo): void {
expect($foo + $foo)->toBe(6);
})->depends('foo');
})->with([3]);
describe('with test after describe', function () {
beforeEach(function () {
describe('with test after describe', function (): void {
beforeEach(function (): void {
$this->count++;
});
describe('foo', function () {});
describe('foo', function (): void {});
it('should run the before each', function () {
it('should run the before each', function (): void {
expect($this->count)->toBe(2);
});
});
+1 -1
View File
@@ -30,7 +30,7 @@ get('foo'); // not incomplete because closure is created...
get('foo')->get('bar')->expect(true)->toBeTrue();
get('foo')->expect(true)->toBeTrue();
describe('a "describe" group of tests', function () {
describe('a "describe" group of tests', function (): void {
get('foo'); // not incomplete because closure is created...
get('foo')->get('bar')->expect(true)->toBeTrue();
get('foo')->expect(true)->toBeTrue();
+6 -4
View File
@@ -1,17 +1,19 @@
<?php
it('may have an associated assignee', function () {
declare(strict_types=1);
it('may have an associated assignee', function (): void {
expect(true)->toBeTrue();
})->done(assignee: 'nunomaduro');
it('may have an associated issue', function () {
it('may have an associated issue', function (): void {
expect(true)->toBeTrue();
})->done(issue: 1);
it('may have an associated PR', function () {
it('may have an associated PR', function (): void {
expect(true)->toBeTrue();
})->done(pr: 1);
it('may have an associated note', function () {
it('may have an associated note', function (): void {
expect(true)->toBeTrue();
})->done(note: 'a note');
+27 -33
View File
@@ -1,95 +1,89 @@
<?php
it('gives access the the underlying expectException', function () {
$this->expectException(InvalidArgumentException::class);
throw new InvalidArgumentException;
it('gives access the the underlying expectException', function (): void {
expect(fn () => throw new InvalidArgumentException)->toThrow(InvalidArgumentException::class);
});
it('catch exceptions', function () {
it('catch exceptions', function (): void {
throw new Exception('Something bad happened');
})->throws(Exception::class);
it('catch exceptions and messages', function () {
it('catch exceptions and messages', function (): void {
throw new Exception('Something bad happened');
})->throws(Exception::class, 'Something bad happened');
it('catch exceptions, messages and code', function () {
it('catch exceptions, messages and code', function (): void {
throw new Exception('Something bad happened', 1);
})->throws(Exception::class, 'Something bad happened', 1);
it('can just define the message', function () {
it('can just define the message', function (): void {
throw new Exception('Something bad happened');
})->throws('Something bad happened');
it('can just define the code', function () {
it('can just define the code', function (): void {
throw new Exception('Something bad happened', 1);
})->throws(1);
it('not catch exceptions if given condition is false', function () {
$this->assertTrue(true);
it('not catch exceptions if given condition is false', function (): void {
expect(true)->toBeTrue();
})->throwsIf(false, Exception::class);
it('catch exceptions if given condition is true', function () {
it('catch exceptions if given condition is true', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(function () {
return true;
}, Exception::class);
})->throwsIf(fn (): true => true, Exception::class);
it('catch exceptions and messages if given condition is true', function () {
it('catch exceptions and messages if given condition is true', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(true, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is true', function () {
it('catch exceptions, messages and code if given condition is true', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsIf(true, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is true', function () {
it('can just define the message if given condition is true', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(true, 'Something bad happened');
it('can just define the code if given condition is true', function () {
it('can just define the code if given condition is true', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsIf(true, 1);
it('can just define the message if given condition is 1', function () {
it('can just define the message if given condition is 1', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(1, 'Something bad happened');
it('can just define the code if given condition is 1', function () {
it('can just define the code if given condition is 1', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsIf(1, 1);
it('not catch exceptions if given condition is true', function () {
$this->assertTrue(true);
it('not catch exceptions if given condition is true', function (): void {
expect(true)->toBeTrue();
})->throwsUnless(true, Exception::class);
it('catch exceptions if given condition is false', function () {
it('catch exceptions if given condition is false', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(function () {
return false;
}, Exception::class);
})->throwsUnless(fn (): false => false, Exception::class);
it('catch exceptions and messages if given condition is false', function () {
it('catch exceptions and messages if given condition is false', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(false, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is false', function () {
it('catch exceptions, messages and code if given condition is false', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsUnless(false, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is false', function () {
it('can just define the message if given condition is false', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(false, 'Something bad happened');
it('can just define the code if given condition is false', function () {
it('can just define the code if given condition is false', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsUnless(false, 1);
it('can just define the message if given condition is 0', function () {
it('can just define the message if given condition is 0', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(0, 'Something bad happened');
it('can just define the code if given condition is 0', function () {
it('can just define the code if given condition is 0', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsUnless(0, 1);
+23 -24
View File
@@ -1,73 +1,72 @@
<?php
it('can access methods', function () {
it('can access methods', function (): void {
expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods');
});
it('can access multiple methods', function () {
it('can access multiple methods', function (): void {
expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods')
->quantity()->toBeInt()->toEqual(20);
});
it('works with not', function () {
it('works with not', function (): void {
expect(new HasMethods)
->name()->not->toEqual('world')->toEqual('Has Methods')
->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull;
});
it('can accept arguments', function () {
it('can accept arguments', function (): void {
expect(new HasMethods)
->multiply(5, 4)->toBeInt->toEqual(20);
});
it('works with each', function () {
it('works with each', function (): void {
expect(new HasMethods)
->attributes()->toBeArray->each->not()->toBeNull
->attributes()->each(function ($attribute) {
->attributes()->each(function ($attribute): void {
$attribute->not->toBeNull();
});
});
it('works inside of each', function () {
it('works inside of each', function (): void {
expect(new HasMethods)
->books()->each(function ($book) {
->books()->each(function ($book): void {
$book->title->not->toBeNull->cost->toBeGreaterThan(19);
});
});
it('works with sequence', function () {
it('works with sequence', function (): void {
expect(new HasMethods)
->books()->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can compose complex expectations', function () {
it('can compose complex expectations', function (): void {
expect(new HasMethods)
->toBeObject()
->name()->toEqual('Has Methods')->not()->toEqual('bar')
->quantity()->not->toEqual('world')->toEqual(20)->toBeInt
->multiply(3, 4)->not->toBeString->toEqual(12)
->attributes()->toBeArray()
->books()->toBeArray->each->not->toBeEmpty
->books()->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can handle nested method calls', function () {
it('can handle nested method calls', function (): void {
expect(new HasMethods)
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
->newInstance()->name()->toEqual('Has Methods')->not->toBeInt
@@ -82,7 +81,7 @@ it('works with higher order tests')
->name()->toEqual('Has Methods')
->books()->each->toBeArray;
it('can use the scoped method to lock into the given level for expectations', function () {
it('can use the scoped method to lock into the given level for expectations', function (): void {
expect(new HasMethods)
->attributes()->scoped(fn ($attributes) => $attributes
->name->toBe('Has Methods')
@@ -99,7 +98,7 @@ it('can use the scoped method to lock into the given level for expectations', fu
);
});
it('works consistently with the json expectation method', function () {
it('works consistently with the json expectation method', function (): void {
expect(new HasMethods)
->jsonString()->json()->id->toBe(1)
->jsonString()->json()->name->toBe('Has Methods')->toBeString()
@@ -113,22 +112,22 @@ class HasMethods
return '{ "id": 1, "name": "Has Methods", "quantity": 20 }';
}
public function name()
public function name(): string
{
return 'Has Methods';
}
public function quantity()
public function quantity(): int
{
return 20;
}
public function multiply($x, $y)
public function multiply($x, $y): int|float
{
return $x * $y;
}
public function attributes()
public function attributes(): array
{
return [
'name' => $this->name(),
@@ -136,7 +135,7 @@ class HasMethods
];
}
public function books()
public function books(): array
{
return [
[
@@ -150,7 +149,7 @@ class HasMethods
];
}
public function newInstance()
public function newInstance(): static
{
return new static;
}
@@ -1,24 +1,24 @@
<?php
it('can access methods and properties', function () {
it('can access methods and properties', function (): void {
expect(new HasMethodsAndProperties)
->name->toEqual('Has Methods and Properties')->not()->toEqual('bar')
->multiply(3, 4)->not->toBeString->toEqual(12)
->posts->each(function ($post) {
->posts->each(function ($post): void {
$post->is_published->toBeTrue;
})->books()->toBeArray()
->posts->toBeArray->each->not->toBeEmpty
->books()->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can handle nested methods and properties', function () {
it('can handle nested methods and properties', function (): void {
expect(new HasMethodsAndProperties)
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
->newInstance()->meta->foo->toBeArray()
@@ -33,15 +33,14 @@ it('works with higher order tests')
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
->newInstance()->books()->toBeArray();
it('can start a new higher order expectation using the and syntax', function () {
it('can start a new higher order expectation using the and syntax', function (): void {
expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class)
->meta->toBeArray
->and(['foo' => 'bar'])
->toBeArray()
->foo->toEqual('bar');
expect(static::getCount())->toEqual(4);
->foo->toEqual('bar')
->and(static::getCount())->toEqual(4);
});
it('can start a new higher order expectation using the and syntax in higher order tests')
@@ -52,12 +51,12 @@ it('can start a new higher order expectation using the and syntax in higher orde
->toBeArray()
->foo->toEqual('bar');
it('can start a new higher order expectation using the and syntax without nesting expectations', function () {
it('can start a new higher order expectation using the and syntax without nesting expectations', function (): void {
expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class)
->meta
->sequence(
function ($value, $key) {
function ($value, $key): void {
$value->toBeArray()->and($key)->toBe('foo');
},
);
@@ -80,7 +79,7 @@ class HasMethodsAndProperties
],
];
public function books()
public function books(): array
{
return [
[
@@ -94,12 +93,12 @@ class HasMethodsAndProperties
];
}
public function multiply($x, $y)
public function multiply($x, $y): int|float
{
return $x * $y;
}
public function newInstance()
public function newInstance(): static
{
return new static;
}
@@ -1,50 +1,50 @@
<?php
it('allows properties to be accessed from the value', function () {
it('allows properties to be accessed from the value', function (): void {
expect(['foo' => 1])->foo->toBeInt()->toEqual(1);
});
it('can access multiple properties from the value', function () {
it('can access multiple properties from the value', function (): void {
expect(['foo' => 'bar', 'hello' => 'world'])
->foo->toBeString()->toEqual('bar')
->hello->toBeString()->toEqual('world');
});
it('works with not', function () {
it('works with not', function (): void {
expect(['foo' => 'bar', 'hello' => 'world'])
->foo->not->not->toEqual('bar')
->foo->not->toEqual('world')->toEqual('bar')
->hello->toEqual('world')->not()->toEqual('bar')->not->toBeNull;
});
it('works with each', function () {
it('works with each', function (): void {
expect(['numbers' => [1, 2, 3, 4], 'words' => ['hey', 'there']])
->numbers->toEqual([1, 2, 3, 4])->each->toBeInt->toBeLessThan(5)
->words->each(function ($word) {
->words->each(function ($word): void {
$word->toBeString()->not->toBeInt();
});
});
it('works inside of each', function () {
it('works inside of each', function (): void {
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
->books->each(function ($book) {
->books->each(function ($book): void {
$book->title->not->toBeNull->cost->toBeGreaterThan(19);
});
});
it('works with sequence', function () {
it('works with sequence', function (): void {
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
->books->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can compose complex expectations', function () {
it('can compose complex expectations', function (): void {
expect(['foo' => 'bar', 'numbers' => [1, 2, 3, 4]])
->toContain('bar')->toBeArray()
->numbers->toEqual([1, 2, 3, 4])->not()->toEqual('bar')->each->toBeInt
@@ -52,23 +52,23 @@ it('can compose complex expectations', function () {
->numbers->toBeArray();
});
it('works with objects', function () {
it('works with objects', function (): void {
expect(new HasProperties)
->name->toEqual('foo')->not->toEqual('world')
->posts->toHaveCount(2)->each(function ($post) {
->posts->toHaveCount(2)->each(function ($post): void {
$post->is_published->toBeTrue();
})
->posts->sequence(
function ($post) {
function ($post): void {
$post->title->toEqual('Foo');
},
function ($post) {
function ($post): void {
$post->title->toEqual('Bar');
},
);
});
it('works with nested properties', function () {
it('works with nested properties', function (): void {
expect(new HasProperties)
->nested->foo->bar->toBeString()->toEqual('baz')
->posts->toBeArray()->toHaveCount(2);
+24 -30
View File
@@ -2,16 +2,15 @@
use Pest\Expectation;
test('an exception is thrown if the the type is not iterable', function () {
test('an exception is thrown if the the type is not iterable', function (): void {
expect('Foobar')->each()->toEqual('Foobar');
})->throws(BadMethodCallException::class, 'Expectation value is not iterable.');
it('expects on each item', function () {
it('expects on each item', function (): void {
expect([1, 1, 1])
->each()
->toEqual(1);
expect(static::getCount())->toBe(3); // + 1 assertion
->toEqual(1)
->and(static::getCount())->toBe(3); // + 1 assertion
expect([1, 1, 1])
->each
@@ -20,13 +19,12 @@ it('expects on each item', function () {
expect(static::getCount())->toBe(7);
});
it('chains expectations on each item', function () {
it('chains expectations on each item', function (): void {
expect([1, 1, 1])
->each()
->toBeInt()
->toEqual(1);
expect(static::getCount())->toBe(6); // + 1 assertion
->toEqual(1)
->and(static::getCount())->toBe(6); // + 1 assertion
expect([2, 2, 2])
->each
@@ -36,13 +34,12 @@ it('chains expectations on each item', function () {
expect(static::getCount())->toBe(13);
});
test('opposite expectations on each item', function () {
test('opposite expectations on each item', function (): void {
expect([1, 2, 3])
->each()
->not()
->toEqual(4);
expect(static::getCount())->toBe(3);
->toEqual(4)
->and(static::getCount())->toBe(3);
expect([1, 2, 3])
->each()
@@ -51,17 +48,16 @@ test('opposite expectations on each item', function () {
expect(static::getCount())->toBe(7);
});
test('chained opposite and non-opposite expectations', function () {
test('chained opposite and non-opposite expectations', function (): void {
expect([1, 2, 3])
->each()
->not()
->toEqual(4)
->toBeInt();
expect(static::getCount())->toBe(6);
->toBeInt()
->and(static::getCount())->toBe(6);
});
it('can add expectations via "and"', function () {
it('can add expectations via "and"', function (): void {
expect([1, 2, 3])
->each()
->toBeInt // + 3
@@ -78,20 +74,18 @@ it('can add expectations via "and"', function () {
expect(static::getCount())->toBe(14);
});
it('accepts callables', function () {
expect([1, 2, 3])->each(function ($number) {
expect($number)->toBeInstanceOf(Expectation::class);
expect($number->value)->toBeInt();
it('accepts callables', function (): void {
expect([1, 2, 3])->each(function ($number): void {
expect($number)->toBeInstanceOf(Expectation::class)
->and($number->value)->toBeInt();
$number->toBeInt->not->toBeString;
});
expect(static::getCount())->toBe(12);
})
->and(static::getCount())->toBe(12);
});
it('passes the key of the current item to callables', function () {
expect([1, 2, 3])->each(function ($number, $key) {
it('passes the key of the current item to callables', function (): void {
expect([1, 2, 3])->each(function ($number, $key): void {
expect($key)->toBeInt();
});
expect(static::getCount())->toBe(3);
})
->and(static::getCount())->toBe(3);
});
+6 -6
View File
@@ -1,29 +1,29 @@
<?php
expect()->extend('toBeAMacroExpectation', function () {
expect()->extend('toBeAMacroExpectation', function (): object {
$this->toBeTrue();
return $this;
});
expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value) {
expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value): object {
$this->toBe($value);
return $this;
});
it('macros true is true', function () {
it('macros true is true', function (): void {
expect(true)->toBeAMacroExpectation();
});
it('macros false is not true', function () {
it('macros false is not true', function (): void {
expect(false)->not->toBeAMacroExpectation();
});
it('macros true is true with argument', function () {
it('macros true is true with argument', function (): void {
expect(true)->toBeAMacroExpectationWithArguments(true);
});
it('macros false is not true with argument', function () {
it('macros false is not true with argument', function (): void {
expect(false)->not->toBeAMacroExpectationWithArguments(true);
});
+2 -2
View File
@@ -2,13 +2,13 @@
use PHPUnit\Framework\ExpectationFailedException;
test('it properly parses json string', function () {
test('it properly parses json string', function (): void {
expect('{"name":"Nuno"}')
->json()
->name
->toBe('Nuno');
});
test('fails with broken json string', function () {
test('fails with broken json string', function (): void {
expect('{":"Nuno"}')->json();
})->throws(ExpectationFailedException::class);
+26 -45
View File
@@ -2,11 +2,11 @@
use PHPUnit\Framework\ExpectationFailedException;
beforeEach(function () {
beforeEach(function (): void {
$this->matched = null;
});
it('pass', function () {
it('pass', function (): void {
expect('baz')
->match('foo', [
'bar' => function ($value) {
@@ -21,25 +21,20 @@ it('pass', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('failures', function () {
it('failures', function (): void {
expect(true)
->match('foo', [
'bar' => function ($value) {
return $value->toBeTrue();
},
'foo' => function ($value) {
return $value->toBeFalse();
},
'bar' => fn ($value) => $value->toBeTrue(),
'foo' => fn ($value) => $value->toBeFalse(),
]
);
})->throws(ExpectationFailedException::class, 'true is false');
it('runs with truthy', function () {
it('runs with truthy', function (): void {
expect('foo')
->match(1, [
'bar' => function ($value) {
@@ -54,12 +49,11 @@ it('runs with truthy', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('runs with falsy', function () {
it('runs with falsy', function (): void {
expect('foo')
->match(false, [
'bar' => function ($value) {
@@ -74,17 +68,14 @@ it('runs with falsy', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('runs with truthy closure condition', function () {
it('runs with truthy closure condition', function (): void {
expect('foo')
->match(
function () {
return '1';
}, [
fn (): string => '1', [
'bar' => function ($value) {
$this->matched = 'bar';
@@ -97,17 +88,14 @@ it('runs with truthy closure condition', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('runs with falsy closure condition', function () {
it('runs with falsy closure condition', function (): void {
expect('foo')
->match(
function () {
return '0';
}, [
fn (): string => '0', [
'bar' => function ($value) {
$this->matched = 'bar';
@@ -120,12 +108,11 @@ it('runs with falsy closure condition', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('can be passed non-callable values', function () {
it('can be passed non-callable values', function (): void {
expect('foo')
->match('pest', [
'bar' => 'foo',
@@ -134,21 +121,15 @@ it('can be passed non-callable values', function () {
);
})->throws(ExpectationFailedException::class, 'two strings are equal');
it('fails with unhandled match', function () {
it('fails with unhandled match', function (): void {
expect('foo')->match('bar', []);
})->throws(ExpectationFailedException::class, 'Unhandled match value.');
it('can be used in higher order tests')
->expect(true)
->match(
function () {
return true;
}, [
false => function ($value) {
return $value->toBeFalse();
},
true => function ($value) {
return $value->toBeTrue();
},
fn (): true => true, [
false => fn ($value) => $value->toBeFalse(),
true => fn ($value) => $value->toBeTrue(),
]
);
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
test('not property calls', function () {
test('not property calls', function (): void {
expect(true)
->toBeTrue()
->not()->toBeFalse()
+15 -15
View File
@@ -61,7 +61,7 @@ $state = new State;
/*
* Overrides toBe to assert two Characters are the same
*/
expect()->pipe('toBe', function ($next, $expected) use ($state) {
expect()->pipe('toBe', function ($next, $expected) use ($state): void {
$state->runCount['char']++;
if ($this->value instanceof Char) {
@@ -81,7 +81,7 @@ expect()->pipe('toBe', function ($next, $expected) use ($state) {
/*
* Overrides toBe to assert two Number objects are the same
*/
expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
expect()->intercept('toBe', Number::class, function ($expected) use ($state): void {
$state->runCount['number']++;
$state->appliedCount['number']++;
@@ -92,7 +92,7 @@ expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
/*
* Overrides toBe to assert all integers are allowed if value is a wildcard (*)
*/
expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numeric($expected), function ($expected) use ($state) {
expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numeric($expected), function ($expected) use ($state): void {
$state->runCount['wildcard']++;
$state->appliedCount['wildcard']++;
});
@@ -100,7 +100,7 @@ expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numer
/*
* Overrides toBe to assert to Symbols are the same
*/
expect()->pipe('toBe', function ($next, $expected) use ($state) {
expect()->pipe('toBe', function ($next, $expected) use ($state): void {
$state->runCount['symbol']++;
if ($this->value instanceof Symbol) {
@@ -117,7 +117,7 @@ expect()->pipe('toBe', function ($next, $expected) use ($state) {
/*
* Overrides toBe to allow ignoring case when checking strings
*/
expect()->intercept('toBe', fn ($value) => is_string($value), function ($expected, $ignoreCase = false) {
expect()->intercept('toBe', fn ($value) => is_string($value), function ($expected, $ignoreCase = false): void {
if ($ignoreCase) {
assertEqualsIgnoringCase($expected, $this->value);
} else {
@@ -125,7 +125,7 @@ expect()->intercept('toBe', fn ($value) => is_string($value), function ($expecte
}
});
test('pipe is applied and can stop pipeline', function () use ($state) {
test('pipe is applied and can stop pipeline', function () use ($state): void {
$char = new Char('A');
$state->reset();
@@ -146,7 +146,7 @@ test('pipe is applied and can stop pipeline', function () use ($state) {
]);
});
test('pipe is run and can let the pipeline keep going', function () use ($state) {
test('pipe is run and can let the pipeline keep going', function () use ($state): void {
$state->reset();
expect(3)->toBe(3)
@@ -165,7 +165,7 @@ test('pipe is run and can let the pipeline keep going', function () use ($state)
]);
});
test('pipe works with negated expectation', function () use ($state) {
test('pipe works with negated expectation', function () use ($state): void {
$char = new Char('A');
$state->reset();
@@ -186,7 +186,7 @@ test('pipe works with negated expectation', function () use ($state) {
]);
});
test('interceptor is applied', function () use ($state) {
test('interceptor is applied', function () use ($state): void {
$number = new Number(1);
$state->reset();
@@ -197,7 +197,7 @@ test('interceptor is applied', function () use ($state) {
->appliedCount->toHaveKey('number', 1);
});
test('interceptor stops the pipeline', function () use ($state) {
test('interceptor stops the pipeline', function () use ($state): void {
$number = new Number(1);
$state->reset();
@@ -218,7 +218,7 @@ test('interceptor stops the pipeline', function () use ($state) {
]);
});
test('interceptor is called only when filter is met', function () use ($state) {
test('interceptor is called only when filter is met', function () use ($state): void {
$state->reset();
expect(1)->toBe(1)
@@ -227,7 +227,7 @@ test('interceptor is called only when filter is met', function () use ($state) {
->appliedCount->toHaveKey('number', 0);
});
test('interceptor can be filtered with a closure', function () use ($state) {
test('interceptor can be filtered with a closure', function () use ($state): void {
$state->reset();
expect('*')->toBe(1)
@@ -236,7 +236,7 @@ test('interceptor can be filtered with a closure', function () use ($state) {
->appliedCount->toHaveKey('wildcard', 1);
});
test('interceptor can be filter the expected parameter as well', function () use ($state) {
test('interceptor can be filter the expected parameter as well', function () use ($state): void {
$state->reset();
expect('*')->toBe('*')
@@ -245,13 +245,13 @@ test('interceptor can be filter the expected parameter as well', function () use
->appliedCount->toHaveKey('wildcard', 0);
});
test('interceptor works with negated expectation', function () {
test('interceptor works with negated expectation', function (): void {
$char = new Number(1);
expect($char)->not->toBe(new Char('B'));
});
test('intercept can add new parameters to the expectation', function () {
test('intercept can add new parameters to the expectation', function (): void {
$ignoreCase = true;
expect('Foo')->toBe('foo', $ignoreCase);
+4 -2
View File
@@ -1,5 +1,7 @@
<?php
test('ray calls do not fail when ray is not installed', function () {
expect(true)->ray()->toBe(true);
declare(strict_types=1);
test('ray calls do not fail when ray is not installed', function (): void {
expect(true)->toBeTrue();
});
+29 -33
View File
@@ -1,95 +1,91 @@
<?php
test('an exception is thrown if the the type is not iterable', function () {
test('an exception is thrown if the the type is not iterable', function (): void {
expect('Foobar')->each->sequence();
})->throws(BadMethodCallException::class, 'Expectation value is not iterable.');
test('an exception is thrown if there are no expectations', function () {
test('an exception is thrown if there are no expectations', function (): void {
expect(['Foobar'])->sequence();
})->throws(InvalidArgumentException::class, 'No sequence expectations defined.');
test('allows for sequences of checks to be run on iterable data', function () {
test('allows for sequences of checks to be run on iterable data', function (): void {
expect([1, 2, 3])
->sequence(
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(1);
},
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(2);
},
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(3);
},
);
expect(static::getCount())->toBe(6);
)
->and(static::getCount())->toBe(6);
});
test('loops back to the start if it runs out of sequence items', function () {
test('loops back to the start if it runs out of sequence items', function (): void {
expect([1, 2, 3, 1, 2, 3, 1, 2])
->sequence(
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(1);
},
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(2);
},
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(3);
},
);
expect(static::getCount())->toBe(16);
)
->and(static::getCount())->toBe(16);
});
test('fails if the number of iterable items is less than the number of expectations', function () {
test('fails if the number of iterable items is less than the number of expectations', function (): void {
expect([1, 2])
->sequence(
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(1);
},
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(2);
},
function ($expectation) {
function ($expectation): void {
$expectation->toBeInt()->toEqual(3);
},
);
})->throws(OutOfRangeException::class, 'Sequence expectations are more than the iterable items.');
test('it works with associative arrays', function () {
test('it works with associative arrays', function (): void {
expect(['foo' => 'bar', 'baz' => 'boom'])
->sequence(
function ($expectation, $key) {
function ($expectation, $key): void {
$expectation->toEqual('bar');
$key->toEqual('foo');
},
function ($expectation, $key) {
function ($expectation, $key): void {
$expectation->toEqual('boom');
$key->toEqual('baz');
},
);
});
test('it can be passed non-callable values', function () {
expect(['foo', 'bar', 'baz'])->sequence('foo', 'bar', 'baz');
expect(static::getCount())->toBe(3);
test('it can be passed non-callable values', function (): void {
expect(['foo', 'bar', 'baz'])->sequence('foo', 'bar', 'baz')
->and(static::getCount())->toBe(3);
});
test('it can be passed a mixture of value types', function () {
test('it can be passed a mixture of value types', function (): void {
expect(['foo', 'bar', 'baz'])->sequence(
'foo',
function ($expectation) {
function ($expectation): void {
$expectation->toEqual('bar')->toBeString();
},
'baz'
);
expect(static::getCount())->toBe(4);
)
->and(static::getCount())->toBe(4);
});
test('it works with traversables', function () {
test('it works with traversables', function (): void {
$generator = (function () {
yield 'one' => (fn () => yield from [1, 2, 3])();
yield 'two' => (fn () => yield from [4, 5, 6])();
+4 -4
View File
@@ -4,21 +4,21 @@ use PHPUnit\Framework\ExpectationFailedException;
expect(true)->toBeTrue()->and(false)->toBeFalse();
test('strict comparisons', function () {
test('strict comparisons', function (): void {
$nuno = new stdClass;
$dries = new stdClass;
expect($nuno)->toBe($nuno)->not->toBe($dries);
});
test('failures', function () {
test('failures', function (): void {
expect(1)->toBe(2);
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(1)->toBe(2, 'oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(1)->not->toBe(1);
})->throws(ExpectationFailedException::class);
+8 -6
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect('abc')->toBeAlpha();
expect('123')->not->toBeAlpha();
test('pass', function (): void {
expect('abc')->toBeAlpha()
->and('123')->not->toBeAlpha();
});
test('failures', function () {
test('failures', function (): void {
expect('123')->toBeAlpha();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('123')->toBeAlpha('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('abc')->not->toBeAlpha();
})->throws(ExpectationFailedException::class);
+8 -6
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect('abc123')->toBeAlphaNumeric();
expect('-')->not->toBeAlphaNumeric();
test('pass', function (): void {
expect('abc123')->toBeAlphaNumeric()
->and('-')->not->toBeAlphaNumeric();
});
test('failures', function () {
test('failures', function (): void {
expect('-')->toBeAlphaNumeric();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('-')->toBeAlphaNumeric('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('abc123')->not->toBeAlphaNumeric();
})->throws(ExpectationFailedException::class);
+10 -8
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect([1, 2, 3])->toBeArray();
expect('1, 2, 3')->not->toBeArray();
test('pass', function (): void {
expect([1, 2, 3])->toBeArray()
->and('1, 2, 3')->not->toBeArray();
});
test('failures', function () {
expect(null)->toBeArray();
test('failures', function (): void {
expect()->toBeArray();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
expect(null)->toBeArray('oh no!');
test('failures with custom message', function (): void {
expect()->toBeArray('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(['a', 'b', 'c'])->not->toBeArray();
})->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('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);
+12 -10
View File
@@ -1,43 +1,45 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('passes with int', function () {
test('passes with int', function (): void {
expect(2)->toBeBetween(1, 3);
});
test('passes with float', function () {
test('passes with float', function (): void {
expect(1.5)->toBeBetween(1.25, 1.75);
});
test('passes with float and int', function () {
test('passes with float and int', function (): void {
expect(1.5)->toBeBetween(1, 2);
});
test('passes with DateTime', function () {
test('passes with DateTime', function (): void {
expect(new DateTime('2023-09-22'))->toBeBetween(new DateTime('2023-09-21'), new DateTime('2023-09-23'));
});
test('failure with int', function () {
test('failure with int', function (): void {
expect(4)->toBeBetween(1, 3);
})->throws(ExpectationFailedException::class);
test('failure with float', function () {
test('failure with float', function (): void {
expect(2)->toBeBetween(1.5, 1.75);
})->throws(ExpectationFailedException::class);
test('failure with float and int', function () {
test('failure with float and int', function (): void {
expect(2.1)->toBeBetween(1, 2);
})->throws(ExpectationFailedException::class);
test('failure with DateTime', function () {
test('failure with DateTime', function (): void {
expect(new DateTime('2023-09-20'))->toBeBetween(new DateTime('2023-09-21'), new DateTime('2023-09-23'));
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(4)->toBeBetween(1, 3, 'oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(2)->not->toBeBetween(1, 3);
})->throws(ExpectationFailedException::class);
+10 -8
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect(true)->toBeBool();
expect(0)->not->toBeBool();
test('pass', function (): void {
expect(true)->toBeBool()
->and(0)->not->toBeBool();
});
test('failures', function () {
expect(null)->toBeBool();
test('failures', function (): void {
expect()->toBeBool();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
expect(null)->toBeBool('oh no!');
test('failures with custom message', function (): void {
expect()->toBeBool('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(false)->not->toBeBool();
})->throws(ExpectationFailedException::class);
+9 -9
View File
@@ -1,26 +1,26 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect(function () {})->toBeCallable();
expect(null)->not->toBeCallable();
test('pass', function (): void {
expect(function (): void {})->toBeCallable()
->and(null)->not->toBeCallable();
});
test('failures', function () {
test('failures', function (): void {
$hello = 5;
expect($hello)->toBeCallable();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
$hello = 5;
expect($hello)->toBeCallable('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
expect(function () {
return 42;
})->not->toBeCallable();
test('not failures', function (): void {
expect(fn (): int => 42)->not->toBeCallable();
})->throws(ExpectationFailedException::class);
+10 -9
View File
@@ -1,23 +1,24 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect('abc')->toBeCamelCase();
expect('abcDef')->toBeCamelCase();
expect('abc-def')->not->toBeCamelCase();
expect('abc-def')->not->toBeCamelCase();
expect('AbcDef')->not->toBeCamelCase();
test('pass', function (): void {
expect('abc')->toBeCamelCase()
->and('abcDef')->toBeCamelCase()
->and('abc-def')->not->toBeCamelCase()->not->toBeCamelCase()
->and('AbcDef')->not->toBeCamelCase();
});
test('failures', function () {
test('failures', function (): void {
expect('Abc')->toBeCamelCase();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('Abc')->toBeCamelCase('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('abcDef')->not->toBeCamelCase();
})->throws(ExpectationFailedException::class);
+8 -6
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect('123')->toBeDigits();
expect('123.14')->not->toBeDigits();
test('pass', function (): void {
expect('123')->toBeDigits()
->and('123.14')->not->toBeDigits();
});
test('failures', function () {
test('failures', function (): void {
expect('123.14')->toBeDigits();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('123.14')->toBeDigits('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('445')->not->toBeDigits();
})->throws(ExpectationFailedException::class);
+6 -4
View File
@@ -1,21 +1,23 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
test('pass', function (): void {
$temp = sys_get_temp_dir();
expect($temp)->toBeDirectory();
});
test('failures', function () {
test('failures', function (): void {
expect('/random/path/whatever')->toBeDirectory();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('/random/path/whatever')->toBeDirectory('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('.')->not->toBeDirectory();
})->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);
+7 -5
View File
@@ -1,24 +1,26 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
test('pass', function (): void {
expect('user@example.com')->toBeEmail()
->and('notanemail')->not->toBeEmail();
});
test('failures', function () {
test('failures', function (): void {
expect('notanemail')->toBeEmail();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('notanemail')->toBeEmail('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('failures with default message', function () {
test('failures with default message', function (): void {
expect('notanemail')->toBeEmail();
})->throws(ExpectationFailedException::class, 'Failed asserting that notanemail is an email address.');
test('not failures', function () {
test('not failures', function (): void {
expect('user@example.com')->not->toBeEmail();
})->throws(ExpectationFailedException::class);
+14 -12
View File
@@ -1,23 +1,25 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect([])->toBeEmpty();
expect(null)->toBeEmpty();
test('pass', function (): void {
expect([])->toBeEmpty()
->and(null)->toBeEmpty();
});
test('failures', function () {
expect([1, 2])->toBeEmpty();
expect(' ')->toBeEmpty();
test('failures', function (): void {
expect([1, 2])->toBeEmpty()
->and(' ')->toBeEmpty();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
expect([1, 2])->toBeEmpty('oh no!');
expect(' ')->toBeEmpty('oh no!');
test('failures with custom message', function (): void {
expect([1, 2])->toBeEmpty('oh no!')
->and(' ')->toBeEmpty('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
expect([])->not->toBeEmpty();
expect(null)->not->toBeEmpty();
test('not failures', function (): void {
expect([])->not->toBeEmpty()
->and(null)->not->toBeEmpty();
})->throws(ExpectationFailedException::class);
+7 -5
View File
@@ -1,19 +1,21 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('strict comparisons', function () {
test('strict comparisons', function (): void {
expect(false)->toBeFalse();
});
test('failures', function () {
test('failures', function (): void {
expect('')->toBeFalse();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('')->toBeFalse('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
expect(false)->not->toBe(false);
test('not failures', function (): void {
expect(false)->toBeTrue();
})->throws(ExpectationFailedException::class);
+8 -6
View File
@@ -1,23 +1,25 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('passes as falsy', function ($value) {
test('passes as falsy', function ($value): void {
expect($value)->toBeFalsy();
})->with([false, '', null, 0, '0']);
test('passes as not falsy', function ($value) {
test('passes as not falsy', function ($value): void {
expect($value)->not->toBeFalsy();
})->with([true, [1], 'false', 1, -1]);
test('failures', function () {
test('failures', function (): void {
expect(1)->toBeFalsy();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(1)->toBeFalsy('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
expect(null)->not->toBeFalsy();
test('not failures', function (): void {
expect()->not->toBeFalsy();
})->throws(ExpectationFailedException::class);
+6 -6
View File
@@ -2,26 +2,26 @@
use PHPUnit\Framework\ExpectationFailedException;
beforeEach(function () {
beforeEach(function (): void {
touch($this->tempFile = sys_get_temp_dir().'/fake.file');
});
afterEach(function () {
afterEach(function (): void {
unlink($this->tempFile);
});
test('pass', function () {
test('pass', function (): void {
expect($this->tempFile)->toBeFile();
});
test('failures', function () {
test('failures', function (): void {
expect('/random/path/whatever.file')->toBeFile();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('/random/path/whatever.file')->toBeFile('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect($this->tempFile)->not->toBeFile();
})->throws(ExpectationFailedException::class);
+8 -6
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect(1.0)->toBeFloat();
expect(1)->not->toBeFloat();
test('pass', function (): void {
expect(1.0)->toBeFloat()
->and(1)->not->toBeFloat();
});
test('failures', function () {
test('failures', function (): void {
expect(42)->toBeFloat();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(42)->toBeFloat('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(log(3))->not->toBeFloat();
})->throws(ExpectationFailedException::class);
+12 -13
View File
@@ -2,33 +2,32 @@
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect(42)->toBeGreaterThan(41);
expect(4)->toBeGreaterThan(3.9);
test('passes', function (): void {
expect(42)->toBeGreaterThan(41)
->and(4)->toBeGreaterThan(3.9);
});
test('passes with DateTime and DateTimeImmutable', function () {
test('passes with DateTime and DateTimeImmutable', function (): void {
$now = new DateTime;
$past = (new DateTimeImmutable)->modify('-1 day');
expect($now)->toBeGreaterThan($past);
expect($past)->not->toBeGreaterThan($now);
expect($now)->toBeGreaterThan($past)
->and($past)->not->toBeGreaterThan($now);
});
test('passes with strings', function () {
expect('b')->toBeGreaterThan('a');
expect('a')->not->toBeGreaterThan('a');
test('passes with strings', function (): void {
expect('b')->toBeGreaterThan('a')
->and('a')->not->toBeGreaterThan('a');
});
test('failures', function () {
test('failures', function (): void {
expect(4)->toBeGreaterThan(4);
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(4)->toBeGreaterThan(4, 'oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(5)->not->toBeGreaterThan(4);
})->throws(ExpectationFailedException::class);
@@ -2,35 +2,33 @@
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect(42)->toBeGreaterThanOrEqual(41);
expect(4)->toBeGreaterThanOrEqual(4);
test('passes', function (): void {
expect(42)->toBeGreaterThanOrEqual(41)
->and(4)->toBeGreaterThanOrEqual(4);
});
test('passes with DateTime and DateTimeImmutable', function () {
test('passes with DateTime and DateTimeImmutable', function (): void {
$now = new DateTime;
$past = (new DateTimeImmutable)->modify('-1 day');
expect($now)->toBeGreaterThanOrEqual($now);
expect($now)->toBeGreaterThanOrEqual($past);
expect($past)->not->toBeGreaterThanOrEqual($now);
expect($now)->toBeGreaterThanOrEqual($now)
->toBeGreaterThanOrEqual($past)
->and($past)->not->toBeGreaterThanOrEqual($now);
});
test('passes with strings', function () {
expect('b')->toBeGreaterThanOrEqual('a');
expect('a')->toBeGreaterThanOrEqual('a');
test('passes with strings', function (): void {
expect('b')->toBeGreaterThanOrEqual('a')
->and('a')->toBeGreaterThanOrEqual('a');
});
test('failures', function () {
test('failures', function (): void {
expect(4)->toBeGreaterThanOrEqual(4.1);
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(4)->toBeGreaterThanOrEqual(4.1, 'oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(5)->not->toBeGreaterThanOrEqual(5);
})->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);
+8 -6
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect('a')->toBeIn(['a', 'b', 'c']);
expect('d')->not->toBeIn(['a', 'b', 'c']);
test('passes', function (): void {
expect('a')->toBeIn(['a', 'b', 'c'])
->and('d')->not->toBeIn(['a', 'b', 'c']);
});
test('failures', function () {
test('failures', function (): void {
expect('d')->toBeIn(['a', 'b', 'c']);
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('d')->toBeIn(['a', 'b', 'c'], 'oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('a')->not->toBeIn(['a', 'b', 'c']);
})->throws(ExpectationFailedException::class);
+8 -6
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect(log(0))->toBeInfinite();
expect(log(1))->not->toBeInfinite();
test('pass', function (): void {
expect(log(0))->toBeInfinite()
->and(log(1))->not->toBeInfinite();
});
test('failures', function () {
test('failures', function (): void {
expect(asin(2))->toBeInfinite();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(asin(2))->toBeInfinite('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(INF)->not->toBeInfinite();
})->throws(ExpectationFailedException::class);
+7 -6
View File
@@ -1,20 +1,21 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect(new Exception)->toBeInstanceOf(Exception::class);
expect(new Exception)->not->toBeInstanceOf(RuntimeException::class);
test('pass', function (): void {
expect(new Exception)->toBeInstanceOf(Exception::class)->not->toBeInstanceOf(RuntimeException::class);
});
test('failures', function () {
test('failures', function (): void {
expect(new Exception)->toBeInstanceOf(RuntimeException::class);
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(new Exception)->toBeInstanceOf(RuntimeException::class, 'oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(new Exception)->not->toBeInstanceOf(Exception::class);
})->throws(ExpectationFailedException::class);
+8 -6
View File
@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect(42)->toBeInt();
expect(42.0)->not->toBeInt();
test('pass', function (): void {
expect(42)->toBeInt()
->and(42.0)->not->toBeInt();
});
test('failures', function () {
test('failures', function (): void {
expect(42.0)->toBeInt();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(42.0)->toBeInt('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect(6 * 7)->not->toBeInt();
})->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();
+10 -6
View File
@@ -1,29 +1,33 @@
<?php
use Pest\Arch\Exceptions\ArchExpectationFailedException;
use Tests\Fixtures\Arch\ToBeInvokable\IsInvokable\InvokableClass;
use Tests\Fixtures\Arch\ToBeInvokable\IsInvokable\InvokableClassViaParent;
use Tests\Fixtures\Arch\ToBeInvokable\IsInvokable\InvokableClassViaTrait;
use Tests\Fixtures\Arch\ToBeInvokable\IsNotInvokable\IsNotInvokableClass;
test('class is invokable')
->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClass')
->expect(InvokableClass::class)
->toBeInvokable();
test('opposite class is invokable')
->throws(ArchExpectationFailedException::class)
->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClass')
->expect(InvokableClass::class)
->not->toBeInvokable();
test('class is invokable via a parent class')
->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClassViaParent')
->expect(InvokableClassViaParent::class)
->toBeInvokable();
test('class is invokable via a trait')
->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsInvokable\\InvokableClassViaTrait')
->expect(InvokableClassViaTrait::class)
->toBeInvokable();
test('failure when the class is not invokable')
->throws(ArchExpectationFailedException::class)
->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsNotInvokable\\IsNotInvokableClass')
->expect(IsNotInvokableClass::class)
->toBeInvokable();
test('class is not invokable')
->expect('Tests\\Fixtures\\Arch\\ToBeInvokable\\IsNotInvokable\\IsNotInvokableClass')
->expect(IsNotInvokableClass::class)
->not->toBeInvokable();
+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);
+8 -6
View File
@@ -1,21 +1,23 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect([])->toBeIterable();
expect(null)->not->toBeIterable();
test('pass', function (): void {
expect([])->toBeIterable()
->and(null)->not->toBeIterable();
});
test('failures', function () {
test('failures', function (): void {
expect(42)->toBeIterable();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(42)->toBeIterable('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
function gen(): iterable
{
yield 1;
+9 -7
View File
@@ -1,21 +1,23 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect('{"hello":"world"}')->toBeJson();
expect('foo')->not->toBeJson();
expect('{"hello"')->not->toBeJson();
test('pass', function (): void {
expect('{"hello":"world"}')->toBeJson()
->and('foo')->not->toBeJson()
->and('{"hello"')->not->toBeJson();
});
test('failures', function () {
test('failures', function (): void {
expect(':"world"}')->toBeJson();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect(':"world"}')->toBeJson('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('{"hello":"world"}')->not->toBeJson();
})->throws(ExpectationFailedException::class);
+11 -9
View File
@@ -1,23 +1,25 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () {
expect('abc')->toBeKebabCase();
expect('abc-def')->toBeKebabCase();
expect('abc_def')->not->toBeKebabCase();
expect('abcDef')->not->toBeKebabCase();
expect('AbcDef')->not->toBeKebabCase();
test('pass', function (): void {
expect('abc')->toBeKebabCase()
->and('abc-def')->toBeKebabCase()
->and('abc_def')->not->toBeKebabCase()
->and('abcDef')->not->toBeKebabCase()
->and('AbcDef')->not->toBeKebabCase();
});
test('failures', function () {
test('failures', function (): void {
expect('Abc')->toBeKebabCase();
})->throws(ExpectationFailedException::class);
test('failures with custom message', function () {
test('failures with custom message', function (): void {
expect('Abc')->toBeKebabCase('oh no!');
})->throws(ExpectationFailedException::class, 'oh no!');
test('not failures', function () {
test('not failures', function (): void {
expect('abc-def')->not->toBeKebabCase();
})->throws(ExpectationFailedException::class);

Some files were not shown because too many files have changed in this diff Show More