Compare commits

..

1 Commits

Author SHA1 Message Date
nuno maduro fbb48ba3e5 fix: escape generated test case filename
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:57:01 +01:00
301 changed files with 2358 additions and 5161 deletions
+7 -7
View File
@@ -2,7 +2,7 @@ name: Static Analysis
on: on:
push: push:
branches: [5.x] branches: [4.x]
pull_request: pull_request:
schedule: schedule:
- cron: '0 9 * * *' - cron: '0 9 * * *'
@@ -33,7 +33,7 @@ jobs:
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with: with:
php-version: 8.4 php-version: 8.3
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
extensions: sockets extensions: sockets
@@ -44,17 +44,17 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies - name: Cache Composer dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with: with:
path: ${{ steps.composer-cache.outputs.dir }} path: ${{ steps.composer-cache.outputs.dir }}
key: static-php-8.4-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }} key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
restore-keys: | restore-keys: |
static-php-8.4-${{ matrix.dependency-version }}-composer- static-php-8.3-${{ matrix.dependency-version }}-composer-
static-php-8.4-composer- static-php-8.3-composer-
- name: Install Dependencies - name: Install Dependencies
env: env:
COMPOSER_ROOT_VERSION: 5.x-dev COMPOSER_ROOT_VERSION: 4.x-dev
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
- name: Profanity Check - name: Profanity Check
+8 -5
View File
@@ -2,7 +2,7 @@ name: Tests
on: on:
push: push:
branches: [5.x] branches: [4.x]
pull_request: pull_request:
schedule: schedule:
- cron: '0 9 * * *' - cron: '0 9 * * *'
@@ -24,9 +24,12 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-latest, macos-latest] # windows-latest os: [ubuntu-latest, macos-latest] # windows-latest
symfony: ['8.0'] symfony: ['7.4', '8.0']
php: ['8.4', '8.5'] php: ['8.3', '8.4', '8.5']
dependency_version: [prefer-stable] dependency_version: [prefer-stable]
exclude:
- php: '8.3'
symfony: '8.0'
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@@ -48,7 +51,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies - name: Cache Composer dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with: with:
path: ${{ steps.composer-cache.outputs.dir }} path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }} key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
@@ -64,7 +67,7 @@ jobs:
- name: Install PHP dependencies - name: Install PHP dependencies
shell: bash shell: bash
env: env:
COMPOSER_ROOT_VERSION: 5.x-dev COMPOSER_ROOT_VERSION: 4.x-dev
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}" run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
- name: Unit Tests - name: Unit Tests
+3 -3
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v5/social.png" width="600" alt="PEST"> <img src="https://raw.githubusercontent.com/pestphp/art/master/v4/social.png" width="600" alt="PEST">
<p align="center"> <p align="center">
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (5.x)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=5.x&label=Tests%205.x"></a> <a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (4.x)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=4.x&label=Tests%204.x"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
@@ -12,7 +12,7 @@
------ ------
> Pest v5 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v5-is-here)**. > Pest v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**.
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. **Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
+3 -3
View File
@@ -2,10 +2,10 @@
When releasing a new version of Pest there are some checks and updates that need to be done: When releasing a new version of Pest there are some checks and updates that need to be done:
> **For Pest v4 you should use the `4.x` branch instead.** > **For Pest v3 you should use the `3.x` branch instead.**
- Clear your local repository with: `git add . && git reset --hard && git checkout 5.x` - Clear your local repository with: `git add . && git reset --hard && git checkout 4.x`
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...5.x](https://github.com/pestphp/pest/compare/{latest_version}...5.x) - On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...4.x](https://github.com/pestphp/pest/compare/{latest_version}...4.x)
- Update the version number in [src/Pest.php](src/Pest.php) - Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test` - Run the tests locally using: `composer test`
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"` - Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
+19 -154
View File
@@ -25,72 +25,26 @@ const PAGE_DIR_CANDIDATES = [
async function loadRolldown() { async function loadRolldown() {
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json')) const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
let path = null const path = projectRequire.resolve('rolldown')
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) return await import(pathToFileURL(path).href)
} }
export function stripJsonComments(raw) {
let out = ''
let inString = false
let quote = ''
let inLine = false
let inBlock = false
for (let i = 0; i < raw.length; i++) {
const c = raw[i]
const n = raw[i + 1]
if (inLine) {
if (c === '\n') { inLine = false; out += c }
continue
}
if (inBlock) {
if (c === '*' && n === '/') { inBlock = false; i++ }
continue
}
if (inString) {
out += c
if (c === '\\') { out += n ?? ''; i++; continue }
if (c === quote) inString = false
continue
}
if (c === '"' || c === "'") { inString = true; quote = c; out += c; continue }
if (c === '/' && n === '/') { inLine = true; i++; continue }
if (c === '/' && n === '*') { inBlock = true; i++; continue }
if (c === '}' || c === ']') out = out.replace(/,\s*$/, '')
out += c
}
return out
}
async function readJsonWithComments(path) { async function readJsonWithComments(path) {
const raw = await readFile(path, 'utf8') const raw = await readFile(path, 'utf8')
return JSON.parse(stripJsonComments(raw)) const stripped = raw
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/[^\n]*/g, '$1')
return JSON.parse(stripped)
} }
export async function loadAliasFromTsconfig(projectRoot = PROJECT_ROOT) { async function loadAliasFromTsconfig() {
const alias = {} const alias = {}
for (const name of ['tsconfig.json', 'jsconfig.json']) { for (const name of ['tsconfig.json', 'jsconfig.json']) {
const p = join(projectRoot, name) const p = join(PROJECT_ROOT, name)
if (!existsSync(p)) continue if (!existsSync(p)) continue
let cfg let cfg
try { cfg = await readJsonWithComments(p) } catch { continue } try { cfg = await readJsonWithComments(p) } catch { continue }
const baseUrl = resolve(projectRoot, cfg?.compilerOptions?.baseUrl ?? '.') const baseUrl = resolve(PROJECT_ROOT, cfg?.compilerOptions?.baseUrl ?? '.')
const paths = cfg?.compilerOptions?.paths ?? {} const paths = cfg?.compilerOptions?.paths ?? {}
for (const [key, targets] of Object.entries(paths)) { for (const [key, targets] of Object.entries(paths)) {
if (!key.endsWith('/*')) continue if (!key.endsWith('/*')) continue
@@ -104,74 +58,6 @@ export async function loadAliasFromTsconfig(projectRoot = PROJECT_ROOT) {
return alias 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) { async function listPageFiles(pagesDir) {
if (!existsSync(pagesDir)) return [] if (!existsSync(pagesDir)) return []
@@ -235,16 +121,8 @@ async function main() {
return return
} }
const loaded = await loadRolldown() const { rolldown } = await loadRolldown()
const alias = await loadAliasFromTsconfig()
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 aliasKeys = Object.keys(alias)
const graph = new Map() const graph = new Map()
@@ -289,7 +167,7 @@ async function main() {
cwd: PROJECT_ROOT, cwd: PROJECT_ROOT,
resolve: { resolve: {
alias, alias,
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.mjs', '.cjs', '.json', '.vue', '.svelte'], extensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json'],
}, },
transform: { jsx: 'preserve' }, transform: { jsx: 'preserve' },
treeshake: false, treeshake: false,
@@ -314,11 +192,6 @@ async function main() {
stack.add(id) stack.add(id)
const acc = new Set() 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) const deps = graph.get(id)
if (deps) { if (deps) {
for (const dep of deps) { for (const dep of deps) {
@@ -327,17 +200,13 @@ async function main() {
const rel = relative(PROJECT_ROOT, dep).split(sep).join('/') const rel = relative(PROJECT_ROOT, dep).split(sep).join('/')
acc.add(rel) acc.add(rel)
} }
if (stack.has(dep)) { if (stack.has(dep)) continue
complete = false
continue
}
const child = computeTransitive(dep, stack) const child = computeTransitive(dep, stack)
if (child) for (const r of child) acc.add(r) if (child) for (const r of child) acc.add(r)
if (!transitiveCache.has(dep)) complete = false
} }
} }
stack.delete(id) stack.delete(id)
if (complete) transitiveCache.set(id, acc) transitiveCache.set(id, acc)
return acc return acc
} }
@@ -361,14 +230,10 @@ async function main() {
process.stdout.write(JSON.stringify(payload)) process.stdout.write(JSON.stringify(payload))
} }
const invokedDirectly = process.argv[1] !== undefined try {
&& import.meta.url === pathToFileURL(process.argv[1]).href void pathToFileURL
await main()
if (invokedDirectly) { } catch (err) {
try { process.stderr.write(String(err?.stack ?? err ?? 'unknown error'))
await main() process.exit(1)
} catch (err) {
process.stderr.write(String(err?.stack ?? err ?? 'unknown error'))
process.exit(1)
}
} }
+16 -17
View File
@@ -17,20 +17,21 @@
} }
], ],
"require": { "require": {
"php": "^8.4", "php": "^8.3.0",
"brianium/paratest": "^7.23.0", "brianium/paratest": "^7.20.0",
"nunomaduro/collision": "^8.9.5", "composer/xdebug-handler": "^3.0.5",
"nunomaduro/collision": "^8.9.4",
"nunomaduro/termwind": "^2.4.0", "nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^5.0.0", "pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^5.0.0", "pestphp/pest-plugin-arch": "^4.0.2",
"pestphp/pest-plugin-mutate": "^5.0.0", "pestphp/pest-plugin-mutate": "^4.0.1",
"pestphp/pest-plugin-profanity": "^5.0.0", "pestphp/pest-plugin-profanity": "^4.2.1",
"phpunit/phpunit": "^13.2.4", "phpunit/phpunit": "^12.5.30",
"symfony/process": "^8.1.0" "symfony/process": "^7.4.13|^8.1.0"
}, },
"conflict": { "conflict": {
"filp/whoops": "<2.18.3", "filp/whoops": "<2.18.3",
"phpunit/phpunit": ">13.2.4", "phpunit/phpunit": ">12.5.30",
"sebastian/exporter": "<7.0.0", "sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0" "webmozart/assert": "<1.11.0"
}, },
@@ -58,13 +59,11 @@
] ]
}, },
"require-dev": { "require-dev": {
"laravel/pao": "^1.1.2", "mrpunyapal/peststan": "^0.2.10",
"pestphp/pest-dev-tools": "^5.0.0", "pestphp/pest-dev-tools": "^4.1.0",
"pestphp/pest-plugin-browser": "^5.0.0", "pestphp/pest-plugin-browser": "^4.3.1",
"pestphp/pest-plugin-phpstan": "^5.0.0", "pestphp/pest-plugin-type-coverage": "^4.0.4",
"pestphp/pest-plugin-rector": "^5.0.0", "psy/psysh": "^0.12.23"
"pestphp/pest-plugin-type-coverage": "^5.0.0",
"psy/psysh": "^0.12.24"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
@@ -1,71 +0,0 @@
<?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. * file that was distributed with this source code.
*/ */
namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber; use PHPUnit\Event\Test\SkippedSubscriber;
@@ -51,20 +51,16 @@ use ReflectionClass;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @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 readonly class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{ {
public function notify(Skipped $event): void public function notify(Skipped $event): void
{ {
if ($event->message() === '__TODO__') { if (str_contains($event->message(), '__TODO__')) {
$this->printTodoItem(); $this->printTodoItem();
} }
$this->printer()->testSkipped(); $this->logger()->testSkipped($event);
} }
/** /**
@@ -73,7 +69,7 @@ final readonly class TestSkippedSubscriber extends Subscriber implements Skipped
private function printTodoItem(): void private function printTodoItem(): void
{ {
$mirror = new ReflectionClass($this->printer()); $mirror = new ReflectionClass($this->printer());
$printProgress = $mirror->getMethod('printProgress'); $printerMirror = $mirror->getMethod('printProgress');
$printProgress->invoke($this->printer(), 'T'); $printerMirror->invoke($this->printer(), 'T');
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
includes: includes:
- phpstan-baseline.neon - phpstan-baseline.neon
- phpstan-pest-extension.neon - phpstan-pest-extension.neon
- vendor/pestphp/pest-plugin-phpstan/extension.neon - vendor/mrpunyapal/peststan/extension.neon
parameters: parameters:
level: 7 level: 7
-38
View File
@@ -2,60 +2,22 @@
declare(strict_types=1); declare(strict_types=1);
use Pest\Rector\Rules\UseToMatchArrayRector;
use Pest\Rector\Set\PestSetList;
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig; 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\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\NarrowObjectReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return RectorConfig::configure() return RectorConfig::configure()
->withPaths([ ->withPaths([
__DIR__.'/src', __DIR__.'/src',
__DIR__.'/tests',
])
->withSets([
PestSetList::CODING_STYLE,
]) ])
->withSkip([ ->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
__DIR__.'/tests/Fixtures/Arch',
ReturnNeverTypeRector::class, ReturnNeverTypeRector::class,
ArrowFunctionDelegatingCallToFirstClassCallableRector::class, ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class, NarrowObjectReturnTypeRector::class,
RemoveParentDelegatingConstructorRector::class, RemoveParentDelegatingConstructorRector::class,
RemoveDuplicatedReturnSelfDocblockRector::class,
RemoveUselessUnionReturnDocblockRector::class,
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( ->withPreparedSets(
deadCode: true, deadCode: true,
@@ -7,7 +7,7 @@
<div> <div>
<span class="text-gray mr-1">- </span> <span class="text-gray mr-1">- </span>
<span>composer require pestphp/pest-plugin-browser:^5.0 --dev</span> <span>composer require pestphp/pest-plugin-browser:^4.0 --dev</span>
</div> </div>
<div> <div>
-1
View File
@@ -18,7 +18,6 @@ final class BootOverrides implements Bootstrapper
* @var array<int, string> * @var array<int, string>
*/ */
public const array FILES = [ public const array FILES = [
'ParaTest/WrapperRunner/ProgressPrinterOutput.php',
'Runner/Filter/NameFilterIterator.php', 'Runner/Filter/NameFilterIterator.php',
'Runner/ResultCache/DefaultResultCache.php', 'Runner/ResultCache/DefaultResultCache.php',
'Runner/TestSuiteLoader.php', 'Runner/TestSuiteLoader.php',
+8 -18
View File
@@ -23,8 +23,6 @@ use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\IncompleteTest; use PHPUnit\Framework\IncompleteTest;
use PHPUnit\Framework\SkippedTest; use PHPUnit\Framework\SkippedTest;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestCase\ExceptionExpectation;
use PHPUnit\Framework\TestCase\OutputBuffer;
use ReflectionException; use ReflectionException;
use ReflectionFunction; use ReflectionFunction;
use ReflectionParameter; use ReflectionParameter;
@@ -433,15 +431,15 @@ trait Testable
unset($this->{$property}); unset($this->{$property});
} }
$outputBuffer = Closure::bind(fn () => $this->outputBuffer, $this, TestCase::class)(); $hasOutputExpectation = Closure::bind(fn (): bool => is_string($this->outputExpectedString) || is_string($this->outputExpectedRegex), $this, TestCase::class)();
if ($outputBuffer->hasExpectation()) { if ($hasOutputExpectation) {
ob_clean(); ob_clean();
Closure::bind(function (): void { Closure::bind(function (): void {
$this->expectedString = null; $this->outputExpectedString = null;
$this->expectedRegularExpression = null; $this->outputExpectedRegex = null;
}, $outputBuffer, OutputBuffer::class)(); }, $this, TestCase::class)();
} }
$this->setUp(); $this->setUp();
@@ -457,9 +455,7 @@ trait Testable
*/ */
private function __isExpectedException(Throwable $e): bool private function __isExpectedException(Throwable $e): bool
{ {
$expectation = Closure::bind(fn () => $this->exceptionExpectation, $this, TestCase::class)(); $read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $this, TestCase::class)();
$read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $expectation, ExceptionExpectation::class)();
$expectedClass = $read('expectedException'); $expectedClass = $read('expectedException');
@@ -467,19 +463,13 @@ trait Testable
return $e instanceof $expectedClass; return $e instanceof $expectedClass;
} }
$expectedMessage = $read('expectedMessage'); $expectedMessage = $read('expectedExceptionMessage');
if ($expectedMessage !== null) { if ($expectedMessage !== null) {
return str_contains($e->getMessage(), (string) $expectedMessage); return str_contains($e->getMessage(), (string) $expectedMessage);
} }
$expectedMessageRegex = $read('expectedMessageRegularExpression'); $expectedCode = $read('expectedExceptionCode');
if ($expectedMessageRegex !== null) {
return preg_match($expectedMessageRegex, $e->getMessage()) === 1;
}
$expectedCode = $read('expectedCode');
if ($expectedCode !== null) { if ($expectedCode !== null) {
return $e->getCode() === $expectedCode; return $e->getCode() === $expectedCode;
+3 -11
View File
@@ -33,7 +33,7 @@ final readonly class Configuration
*/ */
public function in(string ...$targets): UsesCall public function in(string ...$targets): UsesCall
{ {
return new UsesCall($this->filename, [])->in(...$targets); return (new UsesCall($this->filename, []))->in(...$targets);
} }
/** /**
@@ -60,7 +60,7 @@ final readonly class Configuration
*/ */
public function group(string ...$groups): UsesCall public function group(string ...$groups): UsesCall
{ {
return new UsesCall($this->filename, [])->group(...$groups); return (new UsesCall($this->filename, []))->group(...$groups);
} }
/** /**
@@ -68,7 +68,7 @@ final readonly class Configuration
*/ */
public function only(): void public function only(): void
{ {
new BeforeEachCall(TestSuite::getInstance(), $this->filename)->only(); (new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only();
} }
/** /**
@@ -119,14 +119,6 @@ final readonly class Configuration
return new Browser\Configuration; return new Browser\Configuration;
} }
/**
* Gets the evals configuration.
*/
public function evals(): Evals\Configuration // @phpstan-ignore-line
{
return new Evals\Configuration; // @phpstan-ignore-line
}
/** /**
* Gets the TIA (Test Impact Analysis) configuration. * Gets the TIA (Test Impact Analysis) configuration.
*/ */
-19
View File
@@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use RuntimeException;
use Throwable;
/**
* @internal
*/
final class DatasetProviderError extends RuntimeException
{
public function __construct(Throwable $previous)
{
parent::__construct($previous->getMessage(), (int) $previous->getCode(), $previous);
}
}
@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TiaRequiresRepositoryRoot extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct(private readonly string $subdirectoryPrefix)
{
parent::__construct(sprintf(
'Tia mode requires the project root to be the git repository root, but it sits in the subdirectory [%s] of a larger repo.',
$this->subdirectoryPrefix,
));
}
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=red> ERROR </> Tia mode requires the git repository root.',
'',
sprintf(' This project sits in a subdirectory of a larger repo <fg=yellow>%s</>.', $this->subdirectoryPrefix),
'',
' Give the project its own git repository to use Tia.',
'',
]);
}
public function exitCode(): int
{
return 1;
}
}
+13 -5
View File
@@ -130,7 +130,7 @@ final class Expectation
if (getenv('PARATEST') !== false || isset($_SERVER['COLLISION_PRINTER'])) { if (getenv('PARATEST') !== false || isset($_SERVER['COLLISION_PRINTER'])) {
ob_start(); ob_start();
var_dump($this->value, ...$arguments); var_dump($this->value, ...$arguments);
$output = ob_get_clean(); $output = (string) ob_get_clean();
throw new ExpectationFailedException($output); throw new ExpectationFailedException($output);
} }
@@ -244,7 +244,7 @@ final class Expectation
if ($callbacks[$index] instanceof Closure) { if ($callbacks[$index] instanceof Closure) {
$callbacks[$index](new self($value), new self($key)); $callbacks[$index](new self($value), new self($key));
} else { } else {
new self($value)->toEqual($callbacks[$index]); (new self($value))->toEqual($callbacks[$index]);
} }
$index = isset($callbacks[$index + 1]) ? $index + 1 : 0; $index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
@@ -921,7 +921,15 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)), function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if (! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
"to implement '".implode("', '", $interfaces)."'", "to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@@ -1136,8 +1144,8 @@ final class Expectation
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) fn (ObjectDescription $object): bool => isset($object->reflectionClass)
&& $object->reflectionClass->isEnum() && $object->reflectionClass->isEnum()
&& new ReflectionEnum($object->name)->isBacked() // @phpstan-ignore-line && (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
&& (string) new ReflectionEnum($object->name)->getBackingType() === $backingType, // @phpstan-ignore-line && (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
'to be '.$backingType.' backed enum', 'to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
+2 -2
View File
@@ -112,7 +112,7 @@ final class HigherOrderExpectation
* Dynamically calls methods on the class with the given arguments. * Dynamically calls methods on the class with the given arguments.
* *
* @param array<int, mixed> $arguments * @param array<int, mixed> $arguments
* @return self<TOriginalValue, mixed> * @return self<TOriginalValue, mixed>|self<TOriginalValue, TValue>
*/ */
public function __call(string $name, array $arguments): self public function __call(string $name, array $arguments): self
{ {
@@ -127,7 +127,7 @@ final class HigherOrderExpectation
/** /**
* Accesses properties in the value or in the expectation. * Accesses properties in the value or in the expectation.
* *
* @return self<TOriginalValue, mixed> * @return self<TOriginalValue, mixed>|self<TOriginalValue, TValue>
*/ */
public function __get(string $name): self public function __get(string $name): self
{ {
+24 -6
View File
@@ -576,7 +576,15 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => array_all($traits, fn (string $trait): bool => ! isset($object->reflectionClass) || ! in_array($trait, $object->reflectionClass->getTraitNames(), true)), function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (isset($object->reflectionClass) && in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
"not to use traits '".implode("', '", $traits)."'", "not to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@@ -596,7 +604,15 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => ! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)), function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
"not to implement '".implode("', '", $interfaces)."'", "not to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
@@ -798,11 +814,13 @@ final readonly class OppositeExpectation
$exporter = Exporter::default(); $exporter = Exporter::default();
$toString = fn (mixed $argument): string => $exporter->shortenedExport($argument);
throw new ExpectationFailedException(sprintf( throw new ExpectationFailedException(sprintf(
'Expecting %s not %s %s.', 'Expecting %s not %s %s.',
$exporter->shortenedExport($this->original->value), $toString($this->original->value),
strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)), strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)),
implode(' ', array_map(fn (mixed $argument): string => $exporter->export($argument), $arguments)), implode(' ', array_map(fn (mixed $argument): string => $toString($argument), $arguments)),
)); ));
} }
@@ -834,8 +852,8 @@ final readonly class OppositeExpectation
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| ! $object->reflectionClass->isEnum() || ! $object->reflectionClass->isEnum()
|| ! new \ReflectionEnum($object->name)->isBacked() // @phpstan-ignore-line || ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|| (string) new \ReflectionEnum($object->name)->getBackingType() !== $backingType, // @phpstan-ignore-line || (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
'not to be '.$backingType.' backed enum', 'not to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
+1 -2
View File
@@ -161,7 +161,6 @@ final class TestCaseFactory
$classCode = <<<PHP $classCode = <<<PHP
namespace $namespace; namespace $namespace;
use Pest\Exceptions\DatasetProviderError as __PestDatasetProviderError;
use Pest\Repositories\DatasetsRepository as __PestDatasets; use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite; use Pest\TestSuite as __PestTestSuite;
@@ -201,7 +200,7 @@ final class TestCaseFactory
if ( if (
$method->closure instanceof \Closure && $method->closure instanceof \Closure &&
new \ReflectionFunction($method->closure)->isStatic() (new \ReflectionFunction($method->closure))->isStatic()
) { ) {
throw new TestClosureMustNotBeStatic($method); throw new TestClosureMustNotBeStatic($method);
+1 -9
View File
@@ -235,10 +235,6 @@ final class TestCaseMethodFactory
$attributesCode $attributesCode
public function $methodName(...\$arguments) public function $methodName(...\$arguments)
{ {
if (count(\$arguments) === 1 && \$arguments[0] instanceof __PestDatasetProviderError) {
throw \$arguments[0]->getPrevious() ?? \$arguments[0];
}
return \$this->__runTest( return \$this->__runTest(
\$this->__test, \$this->__test,
...\$arguments, ...\$arguments,
@@ -265,11 +261,7 @@ final class TestCaseMethodFactory
public static function $dataProviderName() public static function $dataProviderName()
{ {
try { return __PestDatasets::get(self::\$__filename, "$methodName");
return __PestDatasets::get(self::\$__filename, "$methodName");
} catch (\Throwable \$throwable) {
return [[new __PestDatasetProviderError(\$throwable)]];
}
} }
EOF; EOF;
+22 -21
View File
@@ -15,6 +15,11 @@ use PHPUnit\Event\Test\AfterLastTestMethodErrored;
use PHPUnit\Event\Test\AfterLastTestMethodFailed; use PHPUnit\Event\Test\AfterLastTestMethodFailed;
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
use PHPUnit\Event\Test\BeforeFirstTestMethodFailed; 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\TestSuite;
use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider; use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider;
use PHPUnit\Framework\Exception as FrameworkException; use PHPUnit\Framework\Exception as FrameworkException;
@@ -249,29 +254,25 @@ final readonly class Converter
...$result->testMarkedIncompleteEvents(), ...$result->testMarkedIncompleteEvents(),
]; ];
$notPassedTests = []; $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();
}
foreach ($events as $event) { return $this->getTestCaseLocation($event->test());
if ($event instanceof AfterLastTestMethodErrored) { },
// PHPUnit's collector does not count these towards `numberOfTestsRun`... $events
continue; )
} )
if ($event instanceof AfterLastTestMethodFailed) { );
// PHPUnit's collector does not count these towards `numberOfTestsRun`...
continue;
}
if ($event instanceof BeforeFirstTestMethodErrored || $event instanceof BeforeFirstTestMethodFailed) {
$notPassedTests[] = $event->testClassName();
continue; $numberOfPassedTests = $result->numberOfTestsRun() - $numberOfNotPassedTests;
}
$notPassedTests[] = $this->getTestCaseLocation($event->test());
}
$numberOfPassedTests = $result->numberOfTestsRun()
- count(array_unique($notPassedTests))
- $result->numberOfTestSkippedByTestSuiteSkippedEvents();
return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result); return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result);
} }
@@ -14,10 +14,6 @@ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscribe
{ {
public function notify(Skipped $event): void 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); $this->logger()->testSkipped($event);
} }
} }
-6
View File
@@ -241,12 +241,6 @@ final class TeamCityLogger
$telemetry->memoryUsageSinceStart(), $telemetry->memoryUsageSinceStart(),
$telemetry->durationSincePrevious(), $telemetry->durationSincePrevious(),
$telemetry->memoryUsageSincePrevious(), $telemetry->memoryUsageSincePrevious(),
$telemetry->userCpuTimeSinceStart(),
$telemetry->systemCpuTimeSinceStart(),
$telemetry->totalCpuTimeSinceStart(),
$telemetry->userCpuTimeSincePrevious(),
$telemetry->systemCpuTimeSincePrevious(),
$telemetry->totalCpuTimeSincePrevious(),
); );
} }
+1 -33
View File
@@ -936,7 +936,7 @@ final class Expectation
if ($exception instanceof Closure) { if ($exception instanceof Closure) {
$callback = $exception; $callback = $exception;
$parameters = new ReflectionFunction($exception)->getParameters(); $parameters = (new ReflectionFunction($exception))->getParameters();
if (count($parameters) !== 1) { if (count($parameters) !== 1) {
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
@@ -1142,22 +1142,6 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the value is a ULID.
*
* @return self<TValue>
*/
public function toBeUlid(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
Assert::assertTrue(Str::isUlid($this->value), $message);
return $this;
}
/** /**
* Asserts that the value is between 2 specified values * Asserts that the value is between 2 specified values
* *
@@ -1171,22 +1155,6 @@ final class Expectation
return $this; return $this;
} }
/**
* Asserts that the value is an email address.
*
* @return self<TValue>
*/
public function toBeEmail(string $message = ''): self
{
if ($message === '') {
$message = "Failed asserting that {$this->value} is an email address.";
}
Assert::assertTrue(Str::isEmail((string) $this->value), $message);
return $this;
}
/** /**
* Asserts that the value is a url * Asserts that the value is a url
* *
@@ -19,7 +19,7 @@ use PHPStan\Type\Type;
* $expectation, $opposite, $shouldReset) from being incorrectly resolved as * $expectation, $opposite, $shouldReset) from being incorrectly resolved as
* higher-order value property accesses by downstream ExpressionTypeResolverExtensions. * higher-order value property accesses by downstream ExpressionTypeResolverExtensions.
* *
* This extension must be registered BEFORE the pest-plugin-phpstan HigherOrderExpectationTypeExtension. * This extension must be registered BEFORE the peststan HigherOrderExpectationTypeExtension.
* *
* @internal * @internal
*/ */
@@ -37,7 +37,7 @@ final readonly class HigherOrderExpectationTypeExtension implements ExpressionTy
$varType = $scope->getType($expr->var); $varType = $scope->getType($expr->var);
if (! new ObjectType(HigherOrderExpectation::class)->isSuperTypeOf($varType)->yes()) { if (! (new ObjectType(HigherOrderExpectation::class))->isSuperTypeOf($varType)->yes()) {
return null; return null;
} }
+3 -1
View File
@@ -53,7 +53,9 @@ final class UsesCall
$this->targets = [$filename]; $this->targets = [$filename];
} }
#[\Deprecated(message: 'Use `pest()->printer()->compact()` instead.')] /**
* @deprecated Use `pest()->printer()->compact()` instead.
*/
public function compact(): self public function compact(): self
{ {
DefaultPrinter::compact(true); DefaultPrinter::compact(true);
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Pest;
function version(): string function version(): string
{ {
return '5.0.0-beta.1'; return '4.7.4';
} }
function testDirectory(string $file = ''): string function testDirectory(string $file = ''): string
+1 -1
View File
@@ -50,7 +50,7 @@ final class Cache implements HandlesArguments
} }
} }
if (! $this->hasArgument('--parallel', $arguments) && ! $this->hasArgument('--do-not-cache-result', $arguments) && ! $this->hasArgument('--cache-result', $arguments)) { if (! $this->hasArgument('--parallel', $arguments)) {
return $this->pushArgument('--cache-result', $arguments); return $this->pushArgument('--cache-result', $arguments);
} }
+3 -6
View File
@@ -50,14 +50,11 @@ trait HandleArguments
*/ */
public function popArgument(string $argument, array $arguments): array public function popArgument(string $argument, array $arguments): array
{ {
$key = array_search($argument, $arguments, true); $arguments = array_flip($arguments);
while ($key !== false) { unset($arguments[$argument]);
unset($arguments[$key]);
$key = array_search($argument, $arguments, true);
}
return array_values($arguments); return array_values(array_flip($arguments));
} }
/** /**
+3 -3
View File
@@ -17,8 +17,6 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class Coverage implements AddsOutput, HandlesArguments final class Coverage implements AddsOutput, HandlesArguments
{ {
use Concerns\HandleArguments;
private const string COVERAGE_OPTION = 'coverage'; private const string COVERAGE_OPTION = 'coverage';
private const string MIN_OPTION = 'min'; private const string MIN_OPTION = 'min';
@@ -79,9 +77,11 @@ final class Coverage implements AddsOutput, HandlesArguments
return false; return false;
}))]; }))];
$originals = array_flip($originals);
foreach ($arguments as $argument) { foreach ($arguments as $argument) {
$originals = $this->popArgument($argument, $originals); unset($originals[$argument]);
} }
$originals = array_flip($originals);
$inputs = []; $inputs = [];
$inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE); $inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE);
+7 -1
View File
@@ -178,7 +178,13 @@ final class Parallel implements HandlesArguments
{ {
$arguments = new ArgvInput; $arguments = new ArgvInput;
return array_any(self::UNSUPPORTED_ARGUMENTS, fn (string|array $unsupportedArgument): bool => $arguments->hasParameterOption($unsupportedArgument)); foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) {
if ($arguments->hasParameterOption($unsupportedArgument)) {
return true;
}
}
return false;
} }
/** /**
@@ -120,6 +120,10 @@ final class ResultPrinter
$unexpectedOutput = $this->tail($outputFile); $unexpectedOutput = $this->tail($outputFile);
if ($unexpectedOutput !== '') { if ($unexpectedOutput !== '') {
if (preg_match('/^T+$/', $unexpectedOutput) > 0) {
return;
}
$this->output->write($unexpectedOutput); $this->output->write($unexpectedOutput);
} }
+10 -122
View File
@@ -7,11 +7,11 @@ namespace Pest\Plugins\Parallel\Paratest;
use const DIRECTORY_SEPARATOR; use const DIRECTORY_SEPARATOR;
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection; use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
use ParaTest\Coverage\CoverageMerger;
use ParaTest\JUnit\LogMerger; use ParaTest\JUnit\LogMerger;
use ParaTest\JUnit\Writer; use ParaTest\JUnit\Writer;
use ParaTest\Options; use ParaTest\Options;
use ParaTest\RunnerInterface; use ParaTest\RunnerInterface;
use ParaTest\WrapperRunner\MissingResultsException;
use ParaTest\WrapperRunner\SuiteLoader; use ParaTest\WrapperRunner\SuiteLoader;
use ParaTest\WrapperRunner\WrapperWorker; use ParaTest\WrapperRunner\WrapperWorker;
use Pest\Result; use Pest\Result;
@@ -25,17 +25,11 @@ use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\Util\ExcludeList; use PHPUnit\Util\ExcludeList;
use ReflectionProperty;
use SebastianBergmann\CodeCoverage\Node\Builder;
use SebastianBergmann\CodeCoverage\Serialization\Merger;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingSourceAnalyser;
use SebastianBergmann\Timer\Timer; use SebastianBergmann\Timer\Timer;
use SplFileInfo; use SplFileInfo;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\PhpExecutableFinder;
use function array_filter;
use function array_merge; use function array_merge;
use function array_merge_recursive; use function array_merge_recursive;
use function array_shift; use function array_shift;
@@ -43,8 +37,6 @@ use function assert;
use function count; use function count;
use function dirname; use function dirname;
use function file_get_contents; use function file_get_contents;
use function filesize;
use function is_file;
use function max; use function max;
use function realpath; use function realpath;
use function str_starts_with; use function str_starts_with;
@@ -57,14 +49,14 @@ use function usleep;
*/ */
final class WrapperRunner implements RunnerInterface final class WrapperRunner implements RunnerInterface
{ {
/**
* The time to sleep between cycles.
*/
/** /**
* The merged test result from the parallel run. * The merged test result from the parallel run.
*/ */
public static ?TestResult $result = null; public static ?TestResult $result = null;
/**
* The time to sleep between cycles.
*/
private const int CYCLE_SLEEP = 10000; private const int CYCLE_SLEEP = 10000;
/** /**
@@ -91,18 +83,6 @@ final class WrapperRunner implements RunnerInterface
/** @var array<int,int> */ /** @var array<int,int> */
private array $batches = []; 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> */ /** @var list<SplFileInfo> */
private array $unexpectedOutputFiles = []; private array $unexpectedOutputFiles = [];
@@ -232,7 +212,7 @@ final class WrapperRunner implements RunnerInterface
if ( if (
$this->exitcode > 0 $this->exitcode > 0
&& $this->options->configuration->stopOnFailureThreshold() > 0 && $this->options->configuration->stopOnFailure()
) { ) {
$this->pending = []; $this->pending = [];
} elseif (($pending = array_shift($this->pending)) !== null) { } elseif (($pending = array_shift($this->pending)) !== null) {
@@ -247,18 +227,6 @@ final class WrapperRunner implements RunnerInterface
private function flushWorker(WrapperWorker $worker): void 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->exitcode = max($this->exitcode, $worker->getExitCode());
$this->printer->printFeedback( $this->printer->printFeedback(
$worker->progressFile, $worker->progressFile,
@@ -306,15 +274,10 @@ final class WrapperRunner implements RunnerInterface
$worker->start(); $worker->start();
$this->batches[$token] = 0; $this->batches[$token] = 0;
$this->statusFiles[] = $worker->statusFile; $this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->progressFiles[] = $worker->progressFile;
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile; $this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
$this->testResultFiles[] = $worker->testResultFile; $this->testResultFiles[] = $worker->testResultFile;
if (isset($worker->resultCacheFile)) {
$this->resultCacheFiles[] = $worker->resultCacheFile;
}
if (isset($worker->junitFile)) { if (isset($worker->junitFile)) {
$this->junitFiles[] = $worker->junitFile; $this->junitFiles[] = $worker->junitFile;
} }
@@ -347,20 +310,6 @@ final class WrapperRunner implements RunnerInterface
private function complete(TestResult $testResultSum): int 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) { foreach ($this->testResultFiles as $testResultFile) {
if (! $testResultFile->isFile()) { if (! $testResultFile->isFile()) {
continue; continue;
@@ -395,20 +344,6 @@ final class WrapperRunner implements RunnerInterface
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()), array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueDeprecationEvents(), $testResult->testRunnerTriggeredIssueDeprecationEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueErrorEvents(), $testResult->testRunnerTriggeredIssueErrorEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueNoticeEvents(), $testResult->testRunnerTriggeredIssueNoticeEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssuePhpDeprecationEvents(), $testResult->testRunnerTriggeredIssuePhpDeprecationEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssuePhpNoticeEvents(), $testResult->testRunnerTriggeredIssuePhpNoticeEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssuePhpWarningEvents(), $testResult->testRunnerTriggeredIssuePhpWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueWarningEvents(), $testResult->testRunnerTriggeredIssueWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->errors(), $testResult->errors()), array_merge_recursive($testResultSum->errors(), $testResult->errors()),
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()), array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
@@ -444,15 +379,8 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->testRunnerTriggeredNoticeEvents(), $testResultSum->testRunnerTriggeredNoticeEvents(),
array_values(array_filter( array_values(array_filter(
$testResultSum->testRunnerTriggeredWarningEvents(), $testResultSum->testRunnerTriggeredWarningEvents(),
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found in class') fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
)), )),
$testResultSum->testRunnerTriggeredIssueDeprecationEvents(),
$testResultSum->testRunnerTriggeredIssueErrorEvents(),
$testResultSum->testRunnerTriggeredIssueNoticeEvents(),
$testResultSum->testRunnerTriggeredIssuePhpDeprecationEvents(),
$testResultSum->testRunnerTriggeredIssuePhpNoticeEvents(),
$testResultSum->testRunnerTriggeredIssuePhpWarningEvents(),
$testResultSum->testRunnerTriggeredIssueWarningEvents(),
$testResultSum->errors(), $testResultSum->errors(),
$testResultSum->deprecations(), $testResultSum->deprecations(),
$testResultSum->notices(), $testResultSum->notices(),
@@ -488,11 +416,8 @@ final class WrapperRunner implements RunnerInterface
$exitcode = Result::exitCode($this->options->configuration, $testResultSum); $exitcode = Result::exitCode($this->options->configuration, $testResultSum);
$this->clearFiles($this->statusFiles);
$this->clearFiles($this->progressFiles);
$this->clearFiles($this->unexpectedOutputFiles); $this->clearFiles($this->unexpectedOutputFiles);
$this->clearFiles($this->testResultFiles); $this->clearFiles($this->testResultFiles);
$this->clearFiles($this->resultCacheFiles);
$this->clearFiles($this->coverageFiles); $this->clearFiles($this->coverageFiles);
$this->clearFiles($this->junitFiles); $this->clearFiles($this->junitFiles);
$this->clearFiles($this->teamcityFiles); $this->clearFiles($this->teamcityFiles);
@@ -507,20 +432,6 @@ final class WrapperRunner implements RunnerInterface
return; 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 = new CodeCoverage;
$coverageManager->init( $coverageManager->init(
$this->options->configuration, $this->options->configuration,
@@ -536,33 +447,10 @@ final class WrapperRunner implements RunnerInterface
return; return;
} }
$coverageFiles = []; $coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
foreach ($this->coverageFiles as $fileInfo) { foreach ($this->coverageFiles as $coverageFile) {
$realPath = $fileInfo->getRealPath(); $coverageMerger->addCoverageFromFile($coverageFile);
if ($realPath !== false && $realPath !== '') {
$coverageFiles[] = $realPath;
}
} }
$serializedCoverage = (new Merger)->merge($coverageFiles);
$report = (new Builder(new FileAnalyser(new ParsingSourceAnalyser, false, false)))->build(
$serializedCoverage['codeCoverage'],
$serializedCoverage['testResults'],
$serializedCoverage['basePath'],
);
$codeCoverage = $coverageManager->codeCoverage();
$codeCoverage->excludeUncoveredFiles();
$mergedData = $serializedCoverage['codeCoverage'];
$basePath = $serializedCoverage['basePath'];
if ($basePath !== '') {
foreach ($mergedData->coveredFiles() as $relativePath) {
$mergedData->renameFile($relativePath, $basePath.DIRECTORY_SEPARATOR.$relativePath);
}
}
$codeCoverage->setData($mergedData);
$codeCoverage->setTests($serializedCoverage['testResults']);
(new ReflectionProperty(\SebastianBergmann\CodeCoverage\CodeCoverage::class, 'cachedReport'))->setValue($codeCoverage, $report);
$coverageManager->generateReports( $coverageManager->generateReports(
$this->printer->printer, $this->printer->printer,
@@ -7,7 +7,6 @@ namespace Pest\Plugins\Parallel\Support;
use NunoMaduro\Collision\Adapters\Phpunit\State; use NunoMaduro\Collision\Adapters\Phpunit\State;
use NunoMaduro\Collision\Adapters\Phpunit\Style; use NunoMaduro\Collision\Adapters\Phpunit\Style;
use ParaTest\Options; use ParaTest\Options;
use PHPUnit\Event\Telemetry\CpuTime;
use PHPUnit\Event\Telemetry\GarbageCollectorStatus; use PHPUnit\Event\Telemetry\GarbageCollectorStatus;
use PHPUnit\Event\Telemetry\HRTime; use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Telemetry\Info; use PHPUnit\Event\Telemetry\Info;
@@ -148,20 +147,11 @@ final class CompactPrinter
MemoryUsage::fromBytes(0), MemoryUsage::fromBytes(0),
MemoryUsage::fromBytes(0), MemoryUsage::fromBytes(0),
$garbageCollectorStatus, $garbageCollectorStatus,
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
), ),
$telemetryDuration, $telemetryDuration,
MemoryUsage::fromBytes(0), MemoryUsage::fromBytes(0),
\PHPUnit\Event\Telemetry\Duration::fromSecondsAndNanoseconds(0, 0), \PHPUnit\Event\Telemetry\Duration::fromSecondsAndNanoseconds(0, 0),
MemoryUsage::fromBytes(0), MemoryUsage::fromBytes(0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
); );
$this->style->writeRecap($state, $telemetry, $testResult); $this->style->writeRecap($state, $telemetry, $testResult);
+12 -105
View File
@@ -27,13 +27,6 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
private const string SHARD_OPTION = 'shard'; private const string SHARD_OPTION = 'shard';
/**
* The maximum length allowed for the filter argument.
* While ARG_MAX can be 2MB, individual arguments are often limited to 128KB (MAX_ARG_STRLEN).
* Practical limits in CI environments (like Docker or pipeline runners) can be even lower.
*/
private const int MAX_FILTER_LENGTH = 32768;
/** /**
* The shard index and total number of shards. * The shard index and total number of shards.
* *
@@ -139,8 +132,7 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
self::$timeBalanced = true; self::$timeBalanced = true;
self::$shardsOutdated = $newTests !== []; self::$shardsOutdated = $newTests !== [];
} else { } else {
$isInCurrentShard = fn (int $key): bool => $key % $total === ($index - 1); $testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? [];
$testsToRun = array_values(array_filter($tests, $isInCurrentShard, ARRAY_FILTER_USE_KEY));
} }
self::$shard = [ self::$shard = [
@@ -154,11 +146,7 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
return $arguments; return $arguments;
} }
$filter = $this->buildFilterArgument($testsToRun); return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)];
$this->ensureFilterLengthIsSafe($filter);
return [...$arguments, '--filter', $filter];
} }
/** /**
@@ -199,14 +187,15 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
*/ */
private function allTests(array $arguments): array private function allTests(array $arguments): array
{ {
$command = $this->buildListTestsCommand( $output = (new Process([
$arguments, 'php',
TestSuite::getInstance()->testPath, ...$this->removeParallelArguments($arguments),
); '--list-tests',
]))->setTimeout(120)->mustRun()->getOutput();
$output = new Process($command)->setTimeout(120)->mustRun()->getOutput(); preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
return $this->parseListTestsOutput($output); return array_values(array_unique($matches[1]));
} }
/** /**
@@ -215,97 +204,15 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
*/ */
private function removeParallelArguments(array $arguments): array private function removeParallelArguments(array $arguments): array
{ {
return array_values(array_filter( return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true));
$arguments,
fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true)
&& ! str_starts_with($argument, '--processes'),
));
}
/**
* Builds the subprocess command used to enumerate tests via `--list-tests`.
*
* @param list<string> $arguments
* @return list<string>
*/
private function buildListTestsCommand(array $arguments, string $testPath): array
{
$filtered = $this->removeParallelArguments($arguments);
return ['php', ...$filtered, '--test-directory='.$testPath, '--list-tests'];
}
/**
* Parses `--list-tests` output into a unique list of test class FQCNs.
*
* @return list<string>
*/
private function parseListTestsOutput(string $output): array
{
preg_match_all('/ - (?:P\\\\)?([A-Za-z_]\w*(?:\\\\[A-Za-z_]\w*)*)::/', $output, $matches);
return array_values(array_unique($matches[1]));
} }
/** /**
* Builds the filter argument for the given tests to run. * Builds the filter argument for the given tests to run.
*
* @param array<int, string> $testsToRun
*/ */
private function buildFilterArgument(array $testsToRun): string private function buildFilterArgument(mixed $testsToRun): string
{ {
if ($testsToRun === []) { return addslashes(implode('|', $testsToRun));
return '';
}
/** @var array<string, mixed> $tree */
$tree = [];
foreach ($testsToRun as $class) {
$parts = explode('\\', $class);
$current = &$tree;
foreach ($parts as $part) {
if (! isset($current[$part])) {
$current[$part] = [];
}
$current = &$current[$part];
}
}
$buildRegex = function (array $tree) use (&$buildRegex): string {
$parts = [];
foreach ($tree as $key => $sub) {
$subRegex = $buildRegex($sub);
if ($subRegex === '') {
$parts[] = preg_quote($key, '/');
} else {
$parts[] = preg_quote($key, '/').'\\\\'.(count($sub) > 1 ? '('.$subRegex.')' : $subRegex);
}
}
return implode('|', $parts);
};
return $buildRegex($tree);
}
/**
* Ensures that the filter length is safe for the current environment.
*
* @throws InvalidOption
*/
private function ensureFilterLengthIsSafe(string $filter): void
{
$maxLength = (int) (getenv('PEST_SHARD_MAX_FILTER_LENGTH') ?: self::MAX_FILTER_LENGTH);
if (strlen($filter) > $maxLength) {
throw new InvalidOption(sprintf(
'The generated filter for this shard is too long (%d characters). '.
'This can cause issues with some environments (limit is %d characters). '.
'Please increase the number of shards (e.g., use 1/4 instead of 1/2) to reduce the filter length.',
strlen($filter),
$maxLength
));
}
} }
/** /**
+7 -80
View File
@@ -9,7 +9,6 @@ use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments; use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Contracts\Plugins\Terminable; use Pest\Contracts\Plugins\Terminable;
use Pest\Exceptions\NoAffectedTestsFound; use Pest\Exceptions\NoAffectedTestsFound;
use Pest\Exceptions\TiaRequiresRepositoryRoot;
use Pest\Panic; use Pest\Panic;
use Pest\Plugins\Concerns\HandleArguments; use Pest\Plugins\Concerns\HandleArguments;
use Pest\Plugins\Tia\BaselineSync; use Pest\Plugins\Tia\BaselineSync;
@@ -220,11 +219,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
if (! $watchPatterns->isEnabled()) { if (! $watchPatterns->isEnabled()) {
return false; return false;
} }
if (! $watchPatterns->isLocally()) {
return true;
}
return ! self::argumentPresent('--ci', $arguments); return ! ($watchPatterns->isLocally() && self::argumentPresent('--ci', $arguments));
} }
/** /**
@@ -296,7 +292,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$result = $this->replayGraph->getResult($this->branch, $testId); $result = $this->replayGraph->getResult($this->branch, $testId);
if ($result instanceof TestStatus) { if ($result instanceof TestStatus) {
if ($this->replayGraph->shouldRerunStatus($result)) { if ($result->isFailure() || $result->isError()) {
$this->executedCount++; $this->executedCount++;
return null; return null;
@@ -402,7 +398,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$projectRoot = TestSuite::getInstance()->rootPath; $projectRoot = TestSuite::getInstance()->rootPath;
$perTest = $this->piggybackCoverage $perTest = $this->piggybackCoverage
? $this->mergePerTestFiles($this->coverageCollector->perTestFiles(), $recorder->perTestFiles()) ? $this->coverageCollector->perTestFiles()
: $recorder->perTestFiles(); : $recorder->perTestFiles();
if ($perTest === []) { if ($perTest === []) {
@@ -562,7 +558,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return $exitCode; return $exitCode;
} }
$this->snapshotTestResults(markKnownTestFiles: true); $this->snapshotTestResults();
return $exitCode; return $exitCode;
} }
@@ -628,14 +624,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private function handleParent(array $arguments, string $projectRoot, bool $forceRebuild): array private function handleParent(array $arguments, string $projectRoot, bool $forceRebuild): array
{ {
$this->watchPatterns->useDefaults($projectRoot); $this->watchPatterns->useDefaults($projectRoot);
$this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
$subdirectoryPrefix = $this->gitSubdirectoryPrefix($projectRoot);
if ($subdirectoryPrefix !== null) {
Panic::with(new TiaRequiresRepositoryRoot($subdirectoryPrefix));
}
$this->branch = new ChangedFiles($projectRoot)->currentBranch() ?? 'main';
$fingerprint = Fingerprint::compute($projectRoot); $fingerprint = Fingerprint::compute($projectRoot);
$this->startFingerprint = $fingerprint; $this->startFingerprint = $fingerprint;
@@ -698,7 +687,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
*/ */
private function handleWorker(array $arguments, string $projectRoot, bool $recordingGlobal, bool $replayingGlobal): array private function handleWorker(array $arguments, string $projectRoot, bool $recordingGlobal, bool $replayingGlobal): array
{ {
$this->branch = new ChangedFiles($projectRoot)->currentBranch() ?? 'main'; $this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
if ($replayingGlobal) { if ($replayingGlobal) {
$this->installWorkerReplay($projectRoot); $this->installWorkerReplay($projectRoot);
@@ -715,7 +704,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
} }
if ($this->piggybackCoverage) { if ($this->piggybackCoverage) {
$this->recorder->activateLinkTracking();
$this->recordingActive = true; $this->recordingActive = true;
return $arguments; return $arguments;
@@ -783,7 +771,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private function activateWorkerRecorderForReplay(array $arguments): array private function activateWorkerRecorderForReplay(array $arguments): array
{ {
if ($this->piggybackCoverage) { if ($this->piggybackCoverage) {
$this->recorder->activateLinkTracking();
$this->recordingActive = true; $this->recordingActive = true;
return $arguments; return $arguments;
@@ -836,13 +823,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$affectedFromChanges = $changed === [] ? [] : $graph->affected($changed); $affectedFromChanges = $changed === [] ? [] : $graph->affected($changed);
$rerunFromCache = []; $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) { if ($this->filteredMode) {
$rerunFromCache = $graph->testFilesToRerun($this->branch); $rerunFromCache = $graph->testFilesToRerun($this->branch);
} }
@@ -1023,7 +1003,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
} }
if ($this->piggybackCoverage) { if ($this->piggybackCoverage) {
$recorder->activateLinkTracking();
$this->recordingActive = true; $this->recordingActive = true;
$this->output->writeln(''); $this->output->writeln('');
@@ -1245,8 +1224,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$data = $this->readPartial($key); $data = $this->readPartial($key);
if ($data === null) { if ($data === null) {
$this->state->delete($key);
continue; continue;
} }
@@ -1380,26 +1357,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$this->saveGraph($graph); $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 private function seedResultsInto(Graph $graph): void
{ {
/** @var ResultCollector $collector */ /** @var ResultCollector $collector */
@@ -1411,10 +1368,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
foreach ($results as $testId => $result) { foreach ($results as $testId => $result) {
$file = $result['file'] ?? null; $file = $result['file'] ?? null;
if ($file === null || str_contains($file, "eval()'d")) {
$file = $this->resolveFailedTestFile($testId);
}
if (is_string($file) && $file !== '') { if (is_string($file) && $file !== '') {
$touchedFiles[$file] = true; $touchedFiles[$file] = true;
} }
@@ -1430,13 +1383,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
); );
} }
$graph->markKnownTestFiles(array_keys($touchedFiles));
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results)); $graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$collector->reset(); $collector->reset();
} }
private function snapshotTestResults(bool $markKnownTestFiles = false): void private function snapshotTestResults(): void
{ {
/** @var ResultCollector $collector */ /** @var ResultCollector $collector */
$collector = Container::getInstance()->get(ResultCollector::class); $collector = Container::getInstance()->get(ResultCollector::class);
@@ -1479,10 +1431,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
); );
} }
if ($markKnownTestFiles) {
$graph->markKnownTestFiles(array_keys($touchedFiles));
}
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results)); $graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$this->saveGraph($graph); $this->saveGraph($graph);
@@ -1692,27 +1640,6 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return implode(', ', array_keys($seen)); return implode(', ', array_keys($seen));
} }
/**
* The path from the git repository root down to $projectRoot (e.g.
* `laravel-app`) when the project is nested inside a larger repo, or `null`
* when the project root is itself the repo root (or git is unavailable).
* TIA requires the two to coincide: git reports and addresses paths
* relative to the repo root, while the dependency graph is project-relative.
*/
private function gitSubdirectoryPrefix(string $projectRoot): ?string
{
$process = new Process(['git', 'rev-parse', '--show-prefix'], $projectRoot);
$process->run();
if (! $process->isSuccessful()) {
return null;
}
$prefix = trim($process->getOutput());
return $prefix === '' ? null : rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $prefix), '/');
}
private function composerLockDelta(string $projectRoot, string $sha): string private function composerLockDelta(string $projectRoot, string $sha): string
{ {
$current = @file_get_contents($projectRoot.'/composer.lock'); $current = @file_get_contents($projectRoot.'/composer.lock');
-15
View File
@@ -1,15 +0,0 @@
<?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;
}
-4
View File
@@ -89,10 +89,6 @@ final class FileState implements State
$keys = []; $keys = [];
foreach ($matches as $path) { foreach ($matches as $path) {
if (str_ends_with($path, '.tmp')) {
continue;
}
$keys[] = basename($path); $keys[] = basename($path);
} }
+2 -45
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Pest\Plugins\Tia; namespace Pest\Plugins\Tia;
use Pest\Plugins\Tia\Contracts\Lockfile;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
/** /**
@@ -12,14 +11,7 @@ use Symfony\Component\Finder\Finder;
*/ */
final readonly class Fingerprint final readonly class Fingerprint
{ {
private const int SCHEMA_VERSION = 18; private const int SCHEMA_VERSION = 17;
/**
* @var array<int, class-string<Lockfile>>
*/
private const array LOCKFILES = [
Lockfiles\PackageLock::class,
];
/** /**
* @return array{ * @return array{
@@ -213,11 +205,7 @@ final readonly class Fingerprint
$parts = []; $parts = [];
foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) { foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) {
if (! self::isTrackedByGit($projectRoot, $name)) { $hash = self::trackedHash($projectRoot, $name);
continue;
}
$hash = self::lockfileHash($projectRoot.'/'.$name, $name);
if ($hash !== null) { if ($hash !== null) {
$parts[] = $name.':'.$hash; $parts[] = $name.':'.$hash;
@@ -227,37 +215,6 @@ final readonly class Fingerprint
return $parts === [] ? null : hash('xxh128', implode("\n", $parts)); 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 private static function trackedHash(string $projectRoot, string $relativePath): ?string
{ {
if (! self::isTrackedByGit($projectRoot, $relativePath)) { if (! self::isTrackedByGit($projectRoot, $relativePath)) {
+11 -67
View File
@@ -149,13 +149,6 @@ final class Graph
*/ */
private function applyMigrationChanges(array $migrationPaths, array &$affectedSet): array 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 = []; $changedTables = [];
$unparseable = []; $unparseable = [];
@@ -453,14 +446,13 @@ final class Graph
$bladeAffected = $this->affectedByStaticBladeUsage($rel); $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 !== []) { if ($bladeAffected !== []) {
foreach ($bladeAffected as $testFile) { foreach ($bladeAffected as $testFile) {
$affectedSet[$testFile] = true; $affectedSet[$testFile] = true;
} }
$staticallyHandled[$rel] = true;
} elseif ($this->isBladeComponentPath($rel)) {
$staticallyHandled[$rel] = true; $staticallyHandled[$rel] = true;
} }
} }
@@ -698,16 +690,8 @@ final class Graph
private function shouldRerun(int $status): bool private function shouldRerun(int $status): bool
{ {
return $this->shouldRerunStatus(TestStatus::from($status)); $testStatus = 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()) { if ($testStatus->isFailure() || $testStatus->isError()) {
return true; return true;
} }
@@ -829,38 +813,6 @@ 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 * @param array<string, array<int, string>> $testToTables
*/ */
@@ -1265,21 +1217,7 @@ final class Graph
$tail = substr($tail, 0, -strlen('.blade.php')); $tail = substr($tail, 0, -strlen('.blade.php'));
$name = str_replace('/', '.', $tail); $name = str_replace('/', '.', $tail);
if ($name === '') { return $name === '' ? [] : [$name, str_replace('_', '-', $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> */ /** @return list<string> */
@@ -1351,7 +1289,13 @@ final class Graph
/** @param array<string, array<int, string>> $edges */ /** @param array<string, array<int, string>> $edges */
private function anyTestUses(array $edges, string $component): bool private function anyTestUses(array $edges, string $component): bool
{ {
return array_any($edges, fn (array $components): bool => in_array($component, $components, true)); foreach ($edges as $components) {
if (in_array($component, $components, true)) {
return true;
}
}
return false;
} }
public function pruneMissingTests(): void public function pruneMissingTests(): void
+8 -4
View File
@@ -163,9 +163,7 @@ final class JsModuleGraph
return null; return null;
} }
$nodeModules = $projectRoot.DIRECTORY_SEPARATOR.'node_modules'; if (! is_dir($projectRoot.DIRECTORY_SEPARATOR.'node_modules'.DIRECTORY_SEPARATOR.'vite')) {
if (! is_dir($nodeModules.DIRECTORY_SEPARATOR.'vite') && ! is_dir($nodeModules.DIRECTORY_SEPARATOR.'rolldown')) {
return null; return null;
} }
@@ -388,6 +386,12 @@ final class JsModuleGraph
private static function hasViteConfig(string $projectRoot): bool private static function hasViteConfig(string $projectRoot): bool
{ {
return array_any(self::VITE_CONFIG_NAMES, fn (string $name): bool => is_file($projectRoot.DIRECTORY_SEPARATOR.$name)); foreach (self::VITE_CONFIG_NAMES as $name) {
if (is_file($projectRoot.DIRECTORY_SEPARATOR.$name)) {
return true;
}
}
return false;
} }
} }
-79
View File
@@ -1,79 +0,0 @@
<?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/'));
}
}
+3 -39
View File
@@ -34,8 +34,6 @@ final class Recorder
private bool $active = false; private bool $active = false;
private bool $captureCoverage = false;
private bool $driverChecked = false; private bool $driverChecked = false;
private bool $driverAvailable = false; private bool $driverAvailable = false;
@@ -45,17 +43,6 @@ final class Recorder
private ?SourceScope $sourceScope = null; private ?SourceScope $sourceScope = null;
public function activate(): void 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; $this->active = true;
} }
@@ -88,11 +75,7 @@ final class Recorder
public function beginTest(string $className, string $methodName, string $fallbackFile): void public function beginTest(string $className, string $methodName, string $fallbackFile): void
{ {
if (! $this->active) { if (! $this->active || ! $this->driverAvailable()) {
return;
}
if ($this->captureCoverage && ! $this->driverAvailable()) {
return; return;
} }
@@ -112,10 +95,6 @@ final class Recorder
$this->perTestUsesDatabase[$file] = true; $this->perTestUsesDatabase[$file] = true;
} }
if (! $this->captureCoverage) {
return;
}
if ($this->driver === 'pcov') { if ($this->driver === 'pcov') {
\pcov\clear(); \pcov\clear();
\pcov\start(); \pcov\start();
@@ -128,13 +107,7 @@ final class Recorder
public function endTest(): void public function endTest(): void
{ {
if (! $this->active || $this->currentTestFile === null) { if (! $this->active || ! $this->driverAvailable() || $this->currentTestFile === null) {
return;
}
if (! $this->captureCoverage || ! $this->driverAvailable()) {
$this->currentTestFile = null;
return; return;
} }
@@ -159,15 +132,7 @@ final class Recorder
$data = \xdebug_get_code_coverage(); $data = \xdebug_get_code_coverage();
\xdebug_stop_code_coverage(true); \xdebug_stop_code_coverage(true);
$scope = $this->sourceScope(); $coveredFiles = array_keys($data);
foreach (array_keys($data) as $file) {
if (! $scope->contains($file)) {
unset($data[$file]);
}
}
$coveredFiles = $this->filesWithExecutedLines($data);
} }
foreach ($coveredFiles as $sourceFile) { foreach ($coveredFiles as $sourceFile) {
@@ -386,6 +351,5 @@ final class Recorder
$this->classUsesDatabaseCache = []; $this->classUsesDatabaseCache = [];
$this->sourceScope = null; $this->sourceScope = null;
$this->active = false; $this->active = false;
$this->captureCoverage = false;
} }
} }
+42 -68
View File
@@ -9,12 +9,7 @@ namespace Pest\Plugins\Tia;
*/ */
final class TableExtractor final class TableExtractor
{ {
private const array DML_PREFIXES = ['select', 'insert', 'update', 'delete', 'with', 'replace']; private const array DML_PREFIXES = ['select', 'insert', 'update', 'delete'];
/**
* A single (optionally quoted) identifier segment.
*/
private const string IDENTIFIER = '(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|\w+)';
/** /**
* @return list<string> Sorted, deduped table names referenced by the * @return list<string> Sorted, deduped table names referenced by the
@@ -27,15 +22,22 @@ final class TableExtractor
return []; return [];
} }
if (preg_match('/^[a-zA-Z]+/', $trimmed, $prefixMatch) !== 1) { $prefix = strtolower(substr($trimmed, 0, 6));
$matched = false;
foreach (self::DML_PREFIXES as $dml) {
if (str_starts_with($prefix, $dml)) {
$matched = true;
break;
}
}
if (! $matched) {
return []; return [];
} }
if (! in_array(strtolower($prefixMatch[0]), self::DML_PREFIXES, true)) { $pattern = '/(?:\bfrom|\binto|\bupdate|\bjoin)\s+(?:"([^"]+)"|`([^`]+)`|\[([^\]]+)\]|(\w+))/i';
return [];
}
$pattern = '/\b(?:from|into|update|join)\s+('.self::IDENTIFIER.'(?:\s*\.\s*'.self::IDENTIFIER.')*)/i';
if (preg_match_all($pattern, $sql, $matches) === false) { if (preg_match_all($pattern, $sql, $matches) === false) {
return []; return [];
@@ -43,9 +45,14 @@ final class TableExtractor
$tables = []; $tables = [];
foreach ($matches[1] as $qualified) { for ($i = 0, $n = count($matches[0]); $i < $n; $i++) {
$name = self::unqualified($qualified); $name = $matches[1][$i] !== ''
? $matches[1][$i]
: ($matches[2][$i] !== ''
? $matches[2][$i]
: ($matches[3][$i] !== ''
? $matches[3][$i]
: $matches[4][$i]));
if ($name === '') { if ($name === '') {
continue; continue;
} }
@@ -73,40 +80,40 @@ final class TableExtractor
if (preg_match_all($schemaPattern, $php, $matches) !== false) { if (preg_match_all($schemaPattern, $php, $matches) !== false) {
foreach ($matches[1] as $i => $primary) { foreach ($matches[1] as $i => $primary) {
$tables[strtolower(self::lastDottedSegment($primary))] = true; $tables[strtolower($primary)] = true;
$secondary = $matches[2][$i] ?? ''; $secondary = $matches[2][$i] ?? '';
if ($secondary !== '') { if ($secondary !== '') {
$tables[strtolower(self::lastDottedSegment($secondary))] = true; $tables[strtolower($secondary)] = true;
} }
} }
} }
$qualified = '('.self::IDENTIFIER.'(?:\s*\.\s*'.self::IDENTIFIER.')*)'; $ddlPattern = '/(?:CREATE|ALTER|DROP|TRUNCATE|RENAME)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?\s+["`\[]?(\w+)["`\]]?/i';
$sqlPatterns = [ if (preg_match_all($ddlPattern, $php, $matches) !== false) {
'/(?:CREATE|ALTER|DROP|TRUNCATE|RENAME)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?\s+'.$qualified.'/i', foreach ($matches[1] as $primary) {
'/INSERT\s+(?:IGNORE\s+)?INTO\s+'.$qualified.'/i', $lower = strtolower($primary);
'/UPDATE\s+'.$qualified.'\s+SET\b/i', if (! self::isSchemaMeta($lower)) {
'/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; $tables[$lower] = true;
} }
} }
} }
if (preg_match_all('/DB::table\(\s*[\'"]([^\'"]+)[\'"]\s*\)/', $php, $matches) !== false) { $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;
}
foreach ($matches[1] as $name) { foreach ($matches[1] as $name) {
$lower = strtolower(self::lastDottedSegment($name)); $lower = strtolower($name);
if ($lower !== '' && ! self::isSchemaMeta($lower)) { if (! self::isSchemaMeta($lower)) {
$tables[$lower] = true; $tables[$lower] = true;
} }
} }
@@ -118,39 +125,6 @@ final class TableExtractor
return $out; 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 private static function isSchemaMeta(string $name): bool
{ {
$lower = strtolower($name); $lower = strtolower($name);
+15 -3
View File
@@ -45,7 +45,11 @@ final readonly class TestPaths
$directories[] = $rel; $directories[] = $rel;
} }
$suffixes[] = $directory->suffix(); $suffix = $directory->suffix();
if ($suffix !== '') {
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
}
} }
foreach ($suite->files() as $file) { foreach ($suite->files() as $file) {
@@ -59,7 +63,7 @@ final readonly class TestPaths
if ($suffixes === []) { if ($suffixes === []) {
foreach ($configuration->testSuffixes() as $suffix) { foreach ($configuration->testSuffixes() as $suffix) {
$suffixes[] = $suffix; $suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
} }
} }
} catch (Throwable) { } catch (Throwable) {
@@ -90,7 +94,15 @@ final readonly class TestPaths
if (in_array($relativePath, $this->files, true)) { if (in_array($relativePath, $this->files, true)) {
return true; return true;
} }
$matchesSuffix = array_any($this->suffixes, fn (string $suffix): bool => str_ends_with($relativePath, $suffix));
$matchesSuffix = false;
foreach ($this->suffixes as $suffix) {
if (str_ends_with($relativePath, $suffix)) {
$matchesSuffix = true;
break;
}
}
if (! $matchesSuffix) { if (! $matchesSuffix) {
return false; return false;
+21 -3
View File
@@ -253,17 +253,35 @@ final class WatchPatterns
private function patternTargetsDotfiles(string $pattern): bool private function patternTargetsDotfiles(string $pattern): bool
{ {
return array_any(explode('/', str_replace('\\', '/', $pattern)), fn (string $segment): bool => $segment !== '' && $segment[0] === '.'); foreach (explode('/', str_replace('\\', '/', $pattern)) as $segment) {
if ($segment !== '' && $segment[0] === '.') {
return true;
}
}
return false;
} }
private function touchesVcs(string $file): bool private function touchesVcs(string $file): bool
{ {
return array_any(explode('/', $file), fn (string $segment): bool => in_array($segment, self::VCS_DIRS, true)); foreach (explode('/', $file) as $segment) {
if (in_array($segment, self::VCS_DIRS, true)) {
return true;
}
}
return false;
} }
private function touchesDotfile(string $file): bool private function touchesDotfile(string $file): bool
{ {
return array_any(explode('/', $file), fn (string $segment): bool => $segment !== '' && $segment[0] === '.'); foreach (explode('/', $file) as $segment) {
if ($segment !== '' && $segment[0] === '.') {
return true;
}
}
return false;
} }
private function excludeMatches(string $exclude, string $file): bool private function excludeMatches(string $exclude, string $file): bool
+1 -1
View File
@@ -37,7 +37,7 @@ final class XdebugRestarter implements Restarter
return; return;
} }
new XdebugHandler('pest')->check(); (new XdebugHandler('pest'))->check();
} }
private function xdebugIsCoverageOnly(): bool private function xdebugIsCoverageOnly(): bool
+3 -12
View File
@@ -9,7 +9,6 @@ use Pest\Plugins\Tia\CoverageMerger;
use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\Directory; use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\Node\File; use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Report\Facade;
use SebastianBergmann\Environment\Runtime; use SebastianBergmann\Environment\Runtime;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@@ -96,18 +95,10 @@ final class Coverage
$codeCoverage = require $reportPath; $codeCoverage = require $reportPath;
unlink($reportPath); unlink($reportPath);
// @phpstan-ignore-next-line $totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
if (is_array($codeCoverage)) {
$facade = Facade::fromSerializedData($codeCoverage);
/** @var Directory<File|Directory> $report */ /** @var Directory<File|Directory> $report */
$report = (fn (): Directory => $this->report)->call($facade); $report = $codeCoverage->getReport();
} else {
/** @var Directory<File|Directory> $report */
$report = $codeCoverage->getReport();
}
$totalCoverage = $report->percentageOfExecutedLines();
foreach ($report->getIterator() as $file) { foreach ($report->getIterator() as $file) {
if (! $file instanceof File) { if (! $file instanceof File) {
-13
View File
@@ -86,17 +86,4 @@ final readonly class Exporter
return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->shortenedExport($value)); return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->shortenedExport($value));
} }
/**
* Exports a value into a full single-line string without truncation.
*/
public function export(mixed $value): string
{
$map = [
'#\\\n\s*#' => '',
'# Object \(\.{3}\)#' => '',
];
return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->export($value));
}
} }
+1 -1
View File
@@ -50,7 +50,7 @@ final class HigherOrderMessage
} }
if ($this->hasHigherOrderCallable()) { if ($this->hasHigherOrderCallable()) {
return new HigherOrderCallables($target)->{$this->name}(...$this->arguments); return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
} }
try { try {
+1 -1
View File
@@ -31,7 +31,7 @@ final class HigherOrderMessageCollection
*/ */
public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void
{ {
$this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments)->when($condition); $this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition);
} }
/** /**
+2 -2
View File
@@ -38,7 +38,7 @@ final class HigherOrderTapProxy
return $this->target->{$property}; return $this->target->{$property};
} }
$className = new ReflectionClass($this->target)->getName(); $className = (new ReflectionClass($this->target))->getName();
if (str_starts_with($className, 'P\\')) { if (str_starts_with($className, 'P\\')) {
$className = substr($className, 2); $className = substr($className, 2);
@@ -60,7 +60,7 @@ final class HigherOrderTapProxy
$filename = Backtrace::file(); $filename = Backtrace::file();
$line = Backtrace::line(); $line = Backtrace::line();
return new HigherOrderMessage($filename, $line, $methodName, $arguments) return (new HigherOrderMessage($filename, $line, $methodName, $arguments))
->call($this->target); ->call($this->target);
} }
} }
+2 -2
View File
@@ -181,7 +181,7 @@ final class Reflection
*/ */
public static function getFunctionArguments(Closure $function): array public static function getFunctionArguments(Closure $function): array
{ {
$parameters = new ReflectionFunction($function)->getParameters(); $parameters = (new ReflectionFunction($function))->getParameters();
$arguments = []; $arguments = [];
foreach ($parameters as $parameter) { foreach ($parameters as $parameter) {
@@ -207,7 +207,7 @@ final class Reflection
public static function getFunctionVariable(Closure $function, string $key): mixed public static function getFunctionVariable(Closure $function, string $key): mixed
{ {
return new ReflectionFunction($function)->getStaticVariables()[$key] ?? null; return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null;
} }
/** /**
+4 -36
View File
@@ -10,10 +10,6 @@ use NunoMaduro\Collision\Exceptions\TestOutcome;
use PHPUnit\Event\Code\TestDoxBuilder; use PHPUnit\Event\Code\TestDoxBuilder;
use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\ThrowableBuilder; 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\Errored;
use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitDeprecationTriggered;
@@ -39,7 +35,8 @@ final class StateGenerator
$testResultEvent->throwable() $testResultEvent->throwable()
)); ));
} else { } else {
$this->addClassLevelEvent($state, $testResultEvent); // @phpstan-ignore-next-line
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
} }
} }
@@ -51,7 +48,8 @@ final class StateGenerator
$testResultEvent->throwable() $testResultEvent->throwable()
)); ));
} else { } else {
$this->addClassLevelEvent($state, $testResultEvent); // @phpstan-ignore-next-line
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
} }
} }
@@ -186,36 +184,6 @@ final class StateGenerator
return $state; 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 * @param array<string, list<PhpunitDeprecationTriggered|PhpunitErrorTriggered|PhpunitNoticeTriggered|PhpunitWarningTriggered>> $testResultEvents
*/ */
-16
View File
@@ -98,14 +98,6 @@ final class Str
return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0; return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0;
} }
/**
* Determine if a given value is a valid ULID.
*/
public static function isUlid(string $value): bool
{
return preg_match('/^[0-9A-HJKMNP-TV-Z]{26}$/', $value) > 0;
}
/** /**
* Creates a describe block as `$describeDescription` `$testDescription` format. * Creates a describe block as `$describeDescription` `$testDescription` format.
* *
@@ -118,14 +110,6 @@ final class Str
return sprintf(str_repeat('`%s` → ', count($describeDescriptions)).'%s', ...$descriptionComponents); return sprintf(str_repeat('`%s` → ', count($describeDescriptions)).'%s', ...$descriptionComponents);
} }
/**
* Determine if a given value is a valid email address.
*/
public static function isEmail(string $value): bool
{
return (bool) filter_var($value, FILTER_VALIDATE_EMAIL);
}
/** /**
* Determine if a given value is a valid URL. * Determine if a given value is a valid URL.
*/ */
@@ -6,18 +6,18 @@
FAILED Tests\Fixtures\CollisionTest > error Exception FAILED Tests\Fixtures\CollisionTest > error Exception
error error
at tests/Fixtures/CollisionTest.php:6 at tests/Fixtures/CollisionTest.php:4
1▕ <?php
2▕ 2▕
3▕ declare(strict_types=1); 3▕ test('error', function () {
4▕ 4▕ throw new Exception('error');
5▕ test('error', function (): void { 5▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
6▕ throw new Exception('error'); 6▕
7▕ })->skip(! isset($_SERVER['COLLISION_TEST'])); 7▕ test('success', function () {
8▕ 8▕ expect(true)->toBeTrue();
9▕ test('success', function (): void { 9▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
10▕ expect(true)->toBeTrue();
1 tests/Fixtures/CollisionTest.php:6 1 tests/Fixtures/CollisionTest.php:4
Tests: 1 failed, 1 passed (1 assertions) Tests: 1 failed, 1 passed (1 assertions)
@@ -1,5 +1,5 @@
Pest Testing Framework 5.0.0-beta.1. Pest Testing Framework 4.7.4.
USAGE: pest <file> [options] USAGE: pest <file> [options]
@@ -15,7 +15,6 @@
--cache-directory [dir] ............................ Specify cache directory --cache-directory [dir] ............................ Specify cache directory
--generate-configuration Generate configuration file with suggested settings --generate-configuration Generate configuration file with suggested settings
--migrate-configuration ....... Migrate configuration file to current format --migrate-configuration ....... Migrate configuration file to current format
--validate-configuration ................... Validate XML configuration file
--generate-baseline [file] .................... Generate baseline for issues --generate-baseline [file] .................... Generate baseline for issues
--use-baseline [file] ........................ Use baseline to ignore issues --use-baseline [file] ........................ Use baseline to ignore issues
--ignore-baseline ..................... Do not use baseline to ignore issues --ignore-baseline ..................... Do not use baseline to ignore issues
@@ -41,15 +40,11 @@
--uses [name] ..................... Only run tests that intend to use [name] --uses [name] ..................... Only run tests that intend to use [name]
--requires-php-extension [name] Only run tests that require PHP extension [name] --requires-php-extension [name] Only run tests that require PHP extension [name]
--list-test-files ................................ List available test files --list-test-files ................................ List available test files
--list-test-ids ........................... List available tests as test IDs
--list-tests .......................................... List available tests --list-tests .......................................... List available tests
--list-tests-xml [file] ................. List available tests in XML format --list-tests-xml [file] ................. List available tests in XML format
--filter [pattern] ............................... Filter which tests to run --filter [pattern] ............................... Filter which tests to run
--exclude-filter [pattern] .. Exclude tests for the specified filter pattern --exclude-filter [pattern] .. Exclude tests for the specified filter pattern
--test-suffix [suffixes] Only search for test in files with specified suffix(es). Default: Test.php,.phpt --test-suffix [suffixes] Only search for test in files with specified suffix(es). Default: Test.php,.phpt
--test-files-file [file] Only run test files listed in file (one file by line)
--run-test-id [test-id] Only run the test identified by the specified test ID
--test-id-filter-file [file] Only run tests listed by test ID in file (one test ID per line)
EXECUTION OPTIONS: EXECUTION OPTIONS:
--parallel ........................................... Run tests in parallel --parallel ........................................... Run tests in parallel
@@ -58,21 +53,20 @@
--globals-backup ................. Backup and restore $GLOBALS for each test --globals-backup ................. Backup and restore $GLOBALS for each test
--static-backup ......... Backup and restore static properties for each test --static-backup ......... Backup and restore static properties for each test
--strict-coverage ................... Be strict about code coverage metadata --strict-coverage ................... Be strict about code coverage metadata
--require-coverage-contribution Be strict about tests that do not contribute to code coverage
--strict-global-state .............. Be strict about changes to global state --strict-global-state .............. Be strict about changes to global state
--disallow-test-output ................. Be strict about output during tests --disallow-test-output ................. Be strict about output during tests
--enforce-time-limit ................. Enforce time limit based on test size --enforce-time-limit ................. Enforce time limit based on test size
--default-time-limit [sec] Timeout in seconds for tests that have no declared size --default-time-limit [sec] Timeout in seconds for tests that have no declared size
--do-not-report-useless-tests Do not report tests that do not test anything --do-not-report-useless-tests Do not report tests that do not test anything
--stop-on-defect[=[n]] Stop after first (or n-th) error, failure, warning, or risky test --stop-on-defect ... Stop after first error, failure, warning, or risky test
--stop-on-error[=[n]] ..................... Stop after first (or n-th) error --stop-on-error ..................................... Stop after first error
--stop-on-failure[=[n]] ................. Stop after first (or n-th) failure --stop-on-failure ................................. Stop after first failure
--stop-on-warning[=[n]] ................. Stop after first (or n-th) warning --stop-on-warning ................................. Stop after first warning
--stop-on-risky[=[n]] ................ Stop after first (or n-th) risky test --stop-on-risky ................................ Stop after first risky test
--stop-on-deprecation[=[n]] Stop after first (or n-th) test that triggered a deprecation --stop-on-deprecation ... Stop after first test that triggered a deprecation
--stop-on-notice[=[n]] Stop after first (or n-th) test that triggered a notice --stop-on-notice ............. Stop after first test that triggered a notice
--stop-on-skipped[=[n]] ............ Stop after first (or n-th) skipped test --stop-on-skipped ............................ Stop after first skipped test
--stop-on-incomplete[=[n]] ...... Stop after first (or n-th) incomplete test --stop-on-incomplete ...................... Stop after first incomplete test
--fail-on-empty-test-suite Signal failure using shell exit code when no tests were run --fail-on-empty-test-suite Signal failure using shell exit code when no tests were run
--fail-on-warning Signal failure using shell exit code when a warning was triggered --fail-on-warning Signal failure using shell exit code when a warning was triggered
--fail-on-risky Signal failure using shell exit code when a test was considered risky --fail-on-risky Signal failure using shell exit code when a test was considered risky
@@ -96,7 +90,7 @@
--do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete --do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete
--cache-result ............................ Write test results to cache file --cache-result ............................ Write test results to cache file
--do-not-cache-result .............. Do not write test results to cache file --do-not-cache-result .............. Do not write test results to cache file
--order-by [order] Run tests in order: default|defects|depends|duration-ascending|duration-descending|no-depends|random|reverse|size-ascending|size-descending --order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size
--resolve-dependencies ...................... Alias for "--order-by depends" --resolve-dependencies ...................... Alias for "--order-by depends"
--ignore-dependencies .................... Alias for "--order-by no-depends" --ignore-dependencies .................... Alias for "--order-by no-depends"
--random-order ............................... Alias for "--order-by random" --random-order ............................... Alias for "--order-by random"
@@ -107,7 +101,6 @@
--colors=[flag] ......... Use colors in output ("never", "auto" or "always") --colors=[flag] ......... Use colors in output ("never", "auto" or "always")
--columns [n] ................. Number of columns to use for progress output --columns [n] ................. Number of columns to use for progress output
--columns max ............ Use maximum number of columns for progress output --columns max ............ Use maximum number of columns for progress output
--diff-context [n] Number of context lines shown around changes in diffs (default: 3)
--stderr ................................. Write to STDERR instead of STDOUT --stderr ................................. Write to STDERR instead of STDOUT
--no-progress .................... Disable output of test execution progress --no-progress .................... Disable output of test execution progress
--no-results ................................ Disable output of test results --no-results ................................ Disable output of test results
@@ -122,7 +115,6 @@
--display-warnings ......... Display details for warnings triggered by tests --display-warnings ......... Display details for warnings triggered by tests
--display-all-issues ..... Display details for all issues that are triggered --display-all-issues ..... Display details for all issues that are triggered
--reverse-list .............................. Print defects in reverse order --reverse-list .............................. Print defects in reverse order
--compact ... Replace default progress and result output with compact format
--teamcity . Replace default progress and result output with TeamCity format --teamcity . Replace default progress and result output with TeamCity format
--testdox ................ Replace default result output with TestDox format --testdox ................ Replace default result output with TestDox format
--testdox-summary Repeat TestDox output for tests with errors, failures, or issues --testdox-summary Repeat TestDox output for tests with errors, failures, or issues
@@ -133,12 +125,12 @@
LOGGING OPTIONS: LOGGING OPTIONS:
--log-junit [file] .......... Write test results in JUnit XML format to file --log-junit [file] .......... Write test results in JUnit XML format to file
--log-otr [file] Write test results in Open Test Reporting XML format to file --log-otr [file] Write test results in Open Test Reporting XML format to file
--include-git-information Include Git information in Open Test Reporting XML logfile
--log-teamcity [file] ........ Write test results in TeamCity format to file --log-teamcity [file] ........ Write test results in TeamCity format to file
--testdox-html [file] .. Write test results in TestDox format (HTML) to file --testdox-html [file] .. Write test results in TestDox format (HTML) to file
--testdox-text [file] Write test results in TestDox format (plain text) to file --testdox-text [file] Write test results in TestDox format (plain text) to file
--log-events-text [file] ............... Stream events as plain text to file --log-events-text [file] ............... Stream events as plain text to file
--log-events-verbose-text [file] Stream events as plain text with extended information to file --log-events-verbose-text [file] Stream events as plain text with extended information to file
--include-git-information ..... Include Git information in supported formats
--no-logging ....... Ignore logging configured in the XML configuration file --no-logging ....... Ignore logging configured in the XML configuration file
CODE COVERAGE OPTIONS: CODE COVERAGE OPTIONS:
@@ -159,10 +151,8 @@
--exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format --exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format
--warm-coverage-cache ........................... Warm static analysis cache --warm-coverage-cache ........................... Warm static analysis cache
--coverage-filter [dir] ........... Include [dir] in code coverage reporting --coverage-filter [dir] ........... Include [dir] in code coverage reporting
--branch-coverage ...... Report branch coverage in addition to line coverage --path-coverage .......... Report path coverage in addition to line coverage
--path-coverage ... Report path coverage in addition to line/branch coverage
--disable-coverage-ignore ...... Disable metadata for ignoring code coverage --disable-coverage-ignore ...... Disable metadata for ignoring code coverage
--disable-coverage-targeting .. Disable metadata for code coverage targeting
--no-coverage Ignore code coverage reporting configured in the XML configuration file --no-coverage Ignore code coverage reporting configured in the XML configuration file
AI OPTIONS: AI OPTIONS:
@@ -1,3 +1,3 @@
Pest Testing Framework 5.0.0-beta.1. Pest Testing Framework 4.7.4.
+5 -200
View File
@@ -136,11 +136,6 @@
✓ describe()->with() preserves depends → first with (9) ✓ describe()->with() preserves depends → first with (9)
✓ describe()->with() preserves depends → second with (9) ✓ describe()->with() preserves depends → second with (9)
PASS Tests\Features\DatasetProviderErrors
✓ reports missing datasets as errors for a single file run
✓ reports missing datasets as errors alongside passing tests
✓ reports dataset closure exceptions as errors
PASS Tests\Features\DatasetsTests - 1 todo PASS Tests\Features\DatasetsTests - 1 todo
✓ it throws exception if dataset does not exist ✓ it throws exception if dataset does not exist
✓ it throws exception if dataset already exist ✓ it throws exception if dataset already exist
@@ -298,8 +293,8 @@
✓ it uses correct parent class ✓ it uses correct parent class
DEPR Tests\Features\Deprecated DEPR Tests\Features\Deprecated
✓ 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:12 ! user deprecated → Since foo 1.0: This is a deprecation description // tests/Features/Deprecated.php:10
PASS Tests\Features\Describe - 5 todos PASS Tests\Features\Describe - 5 todos
✓ before each ✓ before each
@@ -522,13 +517,6 @@
✓ pass ✓ pass
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeEmail
✓ pass
✓ failures
✓ failures with custom message
✓ failures with default message
✓ not failures ✓ not failures
PASS Tests\Features\Expect\toBeEmpty PASS Tests\Features\Expect\toBeEmpty
@@ -764,13 +752,6 @@
✓ passes as not truthy with ('0') ✓ passes as not truthy with ('0')
✓ failures ✓ failures
✓ failures with custom message ✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeUlid
✓ failures with wrong type
✓ pass
✓ failures
✓ failures with message
✓ not failures ✓ not failures
PASS Tests\Features\Expect\toBeUppercase PASS Tests\Features\Expect\toBeUppercase
@@ -1279,8 +1260,8 @@
// This is another runtime note // This is another runtime note
NOTI Tests\Features\Notices NOTI Tests\Features\Notices
! notice → This is a notice description // tests/Features/Notices.php:6 ! 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:13 ! a "describe" group of tests → notice → This is a notice description // tests/Features/Notices.php:11
PASS Tests\Features\Pr PASS Tests\Features\Pr
✓ it may be associated with an pr #1, #2 ✓ it may be associated with an pr #1, #2
@@ -1715,8 +1696,6 @@
PASS Tests\Unit\Expectations\OppositeExpectation PASS Tests\Unit\Expectations\OppositeExpectation
✓ it throw expectation failed exception with string argument ✓ it throw expectation failed exception with string argument
✓ it throw expectation failed exception with array argument ✓ it throw expectation failed exception with array argument
✓ it does not truncate long string arguments in error message
✓ it does not truncate custom error message when using not()
PASS Tests\Unit\Overrides\ThrowableBuilder PASS Tests\Unit\Overrides\ThrowableBuilder
✓ collision editor can be added to the stack trace ✓ collision editor can be added to the stack trace
@@ -1728,8 +1707,6 @@
✓ method hasArgument with ('someValue', true) ✓ method hasArgument with ('someValue', true)
✓ method hasArgument with ('--a', false) ✓ method hasArgument with ('--a', false)
✓ method hasArgument with ('--undefined-argument', false) ✓ method hasArgument with ('--undefined-argument', false)
✓ popArgument preserves duplicate values when removing a missing argument
✓ popArgument preserves duplicate values when removing an existing argument
PASS Tests\Unit\Plugins\Environment PASS Tests\Unit\Plugins\Environment
✓ environment is set to CI when --ci option is used ✓ environment is set to CI when --ci option is used
@@ -1738,52 +1715,6 @@
PASS Tests\Unit\Plugins\Retry PASS Tests\Unit\Plugins\Retry
✓ it orders by defects and stop on defects if when --retry is used ✓ it orders by defects and stop on defects if when --retry is used
PASS Tests\Unit\Plugins\Shard
✓ getShard → it parses valid shard format with ('1/2', 1, 2)
✓ getShard → it parses valid shard format with ('2/2', 2, 2)
✓ getShard → it parses valid shard format with ('1/4', 1, 4)
✓ getShard → it parses valid shard format with ('4/4', 4, 4)
✓ getShard → it parses valid shard format with ('1/10', 1, 10)
✓ getShard → it parses valid shard format with ('10/10', 10, 10)
✓ getShard → it parses valid shard format with ('5/100', 5, 100)
✓ getShard → it throws exception for invalid format with (['test', '--shard', 'invalid'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '1'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '1/'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '/2'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', 'a/b'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '1.5/2'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '0/2'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '1/0'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '3/2'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '5/4'])
✓ buildFilterArgument → it generates compact filter for single test
✓ buildFilterArgument → it generates compact filter for multiple tests with common prefix
✓ buildFilterArgument → it generates compact filter for tests with different namespaces
✓ buildFilterArgument → it returns empty string for empty test list
✓ buildFilterArgument → it generates compact filter for deeply nested namespaces
✓ buildFilterArgument → it handles mix of nested and flat namespaces
✓ ensureFilterLengthIsSafe → it accepts filter within length limit
✓ ensureFilterLengthIsSafe → it throws exception when filter exceeds default limit
✓ ensureFilterLengthIsSafe → it respects custom limit from environment variable
✓ ensureFilterLengthIsSafe → it accepts filter within custom limit
✓ handleArguments → it returns original arguments when shard option is not present
✓ handleArguments → it removes parallel arguments from test discovery
✓ parseListTestsOutput → it parses Tests\ namespaced classes from --list-tests output
✓ parseListTestsOutput → it deduplicates repeated class names from multiple test methods
✓ parseListTestsOutput → it returns an empty list for output with no matching lines
✓ parseListTestsOutput → it parses non-Tests namespaced classes
✓ parseListTestsOutput → it parses unnamespaced top-level classes
✓ parseListTestsOutput → it strips the P\ Pest prefix but keeps the rest of the FQCN
✓ parseListTestsOutput → it ignores junk lines that lack the " - …::" framing
✓ buildListTestsCommand → it builds the list-tests command with the forwarded --test-directory
✓ buildListTestsCommand → it strips --parallel and -p when building the list-tests command
✓ buildListTestsCommand → it forwards --test-directory even when input arguments include one
✓ buildListTestsCommand → it strips --processes=N when building the list-tests command
✓ buildListTestsCommand → it strips --processes N (space-separated) when building the list-tests command
✓ addOutput → it displays shard information after test execution
✓ addOutput → it uses singular form for single test file
✓ addOutput → it returns original exit code when shard is not set
PASS Tests\Unit\Plugins\Tia\ContentHash PASS Tests\Unit\Plugins\Tia\ContentHash
✓ of() → it returns false when file does not exist ✓ of() → it returns false when file does not exist
✓ of() → it hashes an existing file ✓ of() → it hashes an existing file
@@ -1821,135 +1752,10 @@
✓ output format → it returns a 32-character hex xxh128 hash ✓ output format → it returns a 32-character hex xxh128 hash
✓ output format → it returns a stable hash for empty content ✓ 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 PASS Tests\Unit\Plugins\Tia\IsEnabledForRun
✓ does not throw when an integer --random-order-seed is passed as a separate argv element ✓ 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 ✓ 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')
✓ it strips JSONC down to something JSON.parse accepts with ('nested')
✓ it strips JSONC down to something JSON.parse accepts with ('empty-object')
✓ it strips JSONC down to something JSON.parse accepts with ('empty-array')
✓ it strips JSONC down to something JSON.parse accepts with ('line-comment-own-line')
✓ it strips JSONC down to something JSON.parse accepts with ('line-comment-trailing')
✓ it strips JSONC down to something JSON.parse accepts with ('leading-line-comments')
✓ it strips JSONC down to something JSON.parse accepts with ('line-comment-eof-no-newline')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-leading')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-inline')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-multiline')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-eof')
✓ it strips JSONC down to something JSON.parse accepts with ('jsdoc-style-block')
✓ it strips JSONC down to something JSON.parse accepts with ('block-contains-double-slash')
✓ it strips JSONC down to something JSON.parse accepts with ('line-then-block')
✓ it strips JSONC down to something JSON.parse accepts with ('comment-with-quotes')
✓ it strips JSONC down to something JSON.parse accepts with ('comment-with-braces-commas')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-object')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-array')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-nested')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-newline')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-then-block')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-then-line')
✓ it strips JSONC down to something JSON.parse accepts with ('crlf-line-comment')
✓ it strips JSONC down to something JSON.parse accepts with ('tabs-whitespace')
✓ it strips JSONC down to something JSON.parse accepts with ('url-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('glob-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('path-alias-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-open-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-close-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-both-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('star-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('single-slashes-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('escaped-quote-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('escaped-backslash-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('backslash-then-slash')
✓ it strips JSONC down to something JSON.parse accepts with ('newline-escape-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('unicode-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-secret')
✓ it never touches comment-looking sequences inside string values
✓ it removes the comment body entirely
✓ it builds the expected alias map from a tsconfig with ('basic')
✓ it builds the expected alias map from a tsconfig with ('multiple-aliases')
✓ it builds the expected alias map from a tsconfig with ('default-baseUrl')
✓ it builds the expected alias map from a tsconfig with ('baseUrl-subdir')
✓ it builds the expected alias map from a tsconfig with ('jsconfig-fallback')
✓ it builds the expected alias map from a tsconfig with ('first-target-wins')
✓ it builds the expected alias map from a tsconfig with ('multiple-keys-mixed')
✓ it builds the expected alias map from a tsconfig with ('no-paths')
✓ it builds the expected alias map from a tsconfig with ('no-compiler-options')
✓ it builds the expected alias map from a tsconfig with ('key-without-glob')
✓ it builds the expected alias map from a tsconfig with ('target-without-glob')
✓ it builds the expected alias map from a tsconfig with ('target-not-array')
✓ it builds the expected alias map from a tsconfig with ('target-empty-array')
✓ it builds the expected alias map from a tsconfig with ('commented-tsconfig')
✓ it builds the expected alias map from a tsconfig with ('trailing-comma-paths')
✓ 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 PASS Tests\Unit\Preset
✓ preset invalid name ✓ preset invalid name
✓ preset → myFramework ✓ preset → myFramework
@@ -2102,7 +1908,6 @@
✓ parallel ✓ parallel
✓ a parallel test can extend another test with same name ✓ a parallel test can extend another test with same name
✓ parallel reports invalid datasets as failures ✓ parallel reports invalid datasets as failures
✓ parallel can have multiple exclude-groups
PASS Tests\Visual\ParallelNestedDatasets PASS Tests\Visual\ParallelNestedDatasets
✓ parallel loads nested datasets from nested directories ✓ parallel loads nested datasets from nested directories
@@ -2136,4 +1941,4 @@
✓ pass with dataset with ('my-datas-set-value') ✓ pass with dataset with ('my-datas-set-value')
✓ within describe → pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value')
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1504 passed (3281 assertions) Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1330 passed (3010 assertions)
-9
View File
@@ -1,9 +0,0 @@
<?php
dataset('throws', function () {
throw new RuntimeException('boom from dataset');
});
it('x', function ($a) {
expect($a)->toBeTrue();
})->with('throws');
-7
View File
@@ -1,7 +0,0 @@
<?php
declare(strict_types=1);
it('references a missing dataset', function ($value) {
expect($value)->toBeTruthy();
})->with('missing');
-11
View File
@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
it('passes normally', function () {
expect(true)->toBeTrue();
});
it('references a missing dataset', function ($value) {
expect($value)->toBeTruthy();
})->with('missing');
-2
View File
@@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1);
use Pest\Expectation; use Pest\Expectation;
use Pest\Plugins\Tia\BaselineSync; use Pest\Plugins\Tia\BaselineSync;
+3 -5
View File
@@ -1,14 +1,12 @@
<?php <?php
declare(strict_types=1);
use Pest\Plugin; use Pest\Plugin;
trait PluginTrait trait PluginTrait
{ {
public function assertPluginTraitGotRegistered(): void public function assertPluginTraitGotRegistered(): void
{ {
expect(true)->toBeTrue(); $this->assertTrue(true);
} }
} }
@@ -16,14 +14,14 @@ trait SecondPluginTrait
{ {
public function assertSecondPluginTraitGotRegistered(): void public function assertSecondPluginTraitGotRegistered(): void
{ {
expect(true)->toBeTrue(); $this->assertTrue(true);
} }
} }
Plugin::uses(PluginTrait::class); Plugin::uses(PluginTrait::class);
Plugin::uses(SecondPluginTrait::class); Plugin::uses(SecondPluginTrait::class);
function _assertThat(): void function _assertThat()
{ {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
} }
+12 -6
View File
@@ -1,13 +1,19 @@
<?php <?php
declare(strict_types=1);
dataset('bound.closure', function () { dataset('bound.closure', function () {
yield fn (): int => 1; yield function () {
yield fn (): int => 2; return 1;
};
yield function () {
return 2;
};
}); });
dataset('bound.array', [ dataset('bound.array', [
fn (): int => 1, function () {
fn (): int => 2, return 1;
},
function () {
return 2;
},
]); ]);
-2
View File
@@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1);
dataset('numbers.closure', function () { dataset('numbers.closure', function () {
yield [1]; yield [1];
yield [2]; yield [2];
+1 -3
View File
@@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1); test('global functions are loaded', function () {
test('global functions are loaded', function (): void {
expect(helper_returns_string())->toBeString(); expect(helper_returns_string())->toBeString();
}); });
+25 -25
View File
@@ -1,10 +1,10 @@
<?php <?php
beforeEach(function (): void { beforeEach(function () {
$this->count = 0; $this->count = 0;
}); });
afterEach(function (): void { afterEach(function () {
match ($this->name()) { match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(1), '__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), '__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(1),
@@ -21,37 +21,37 @@ afterEach(function (): void {
$this->count++; $this->count++;
}); });
it('can run after test', function (): void { it('can run after test', function () {
expect($this->count)->toBe(0); expect($this->count)->toBe(0);
$this->count++; $this->count++;
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(2); expect($this->count)->toBe(2);
$this->count++; $this->count++;
}); });
it('can run after test twice', function (): void { it('can run after test twice', function () {
expect($this->count)->toBe(0); expect($this->count)->toBe(0);
$this->count++; $this->count++;
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(2); expect($this->count)->toBe(2);
$this->count++; $this->count++;
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(3); expect($this->count)->toBe(3);
$this->count++; $this->count++;
}); });
it('does not run when skipped', function (): void { it('does not run when skipped', function () {
dd('This should not run 1'); dd('This should not run 1');
})->skip()->after(function (): void { })->skip()->after(function () {
dd('This should not run 2'); dd('This should not run 2');
}); });
afterEach(function (): void { afterEach(function () {
match ($this->name()) { match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(3), '__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), '__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(4),
@@ -70,7 +70,7 @@ afterEach(function (): void {
$this->count++; $this->count++;
}); });
afterEach(function (): void { afterEach(function () {
match ($this->name()) { match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(4), '__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), '__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(5),
@@ -87,47 +87,47 @@ afterEach(function (): void {
$this->count++; $this->count++;
}); });
describe('something', function (): void { describe('something', function () {
it('does not run when skipped', function (): void { it('does not run when skipped', function () {
dd('This should not run 3'); dd('This should not run 3');
})->skip()->after(function (): void { })->skip()->after(function () {
dd('This should not run 4'); dd('This should not run 4');
}); });
it('can run after test', function (): void { it('can run after test', function () {
expect($this->count)->toBe(0); expect($this->count)->toBe(0);
$this->count++; $this->count++;
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(5); expect($this->count)->toBe(5);
$this->count++; $this->count++;
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(6); expect($this->count)->toBe(6);
$this->count++; $this->count++;
}); });
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(4); expect($this->count)->toBe(4);
$this->count++; $this->count++;
}); });
describe('something 2', function (): void { describe('something 2', function () {
it('can run after test', function (): void { it('can run after test', function () {
expect($this->count)->toBe(0); expect($this->count)->toBe(0);
$this->count++; $this->count++;
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(6); expect($this->count)->toBe(6);
$this->count++; $this->count++;
}); });
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(4); expect($this->count)->toBe(4);
$this->count++; $this->count++;
})->after(function (): void { })->after(function () {
expect($this->count)->toBe(5); expect($this->count)->toBe(5);
$this->count++; $this->count++;
@@ -136,7 +136,7 @@ describe('something 2', function (): void {
test('high order test') test('high order test')
->defer(fn () => $this->count++) ->defer(fn () => $this->count++)
->expect(fn () => $this->count)->toBe(1) ->expect(fn () => $this->count)->toBe(1)
->after(function (): void { ->after(function () {
expect($this->count)->toBe(4); expect($this->count)->toBe(4);
$this->count++; $this->count++;
@@ -146,7 +146,7 @@ test('high order test with skip')
->skip() ->skip()
->defer(fn () => $this->count++) ->defer(fn () => $this->count++)
->expect(fn () => $this->count)->toBe(1) ->expect(fn () => $this->count)->toBe(1)
->after(function (): void { ->after(function () {
dd('This should not run 5'); dd('This should not run 5');
}); });
+5 -7
View File
@@ -1,21 +1,19 @@
<?php <?php
declare(strict_types=1);
$file = __DIR__.DIRECTORY_SEPARATOR.'after-all-test'; $file = __DIR__.DIRECTORY_SEPARATOR.'after-all-test';
beforeAll(function () use ($file): void { beforeAll(function () use ($file) {
@unlink($file); @unlink($file);
}); });
afterAll(function () use ($file): void { afterAll(function () use ($file) {
@unlink($file); @unlink($file);
}); });
test('deletes file after all', function () use ($file): void { test('deletes file after all', function () use ($file) {
file_put_contents($file, 'foo'); file_put_contents($file, 'foo');
expect($file)->toBeFile(); $this->assertFileExists($file);
register_shutdown_function(function (): void { register_shutdown_function(function () {
// $this->assertFileDoesNotExist($file); // $this->assertFileDoesNotExist($file);
}); });
}); });
+34 -34
View File
@@ -2,99 +2,99 @@
$state = new stdClass; $state = new stdClass;
beforeEach(function () use ($state): void { beforeEach(function () use ($state) {
$this->state = $state; $this->state = $state;
}); });
afterEach(function (): void { afterEach(function () {
$this->state->bar = 1; $this->state->bar = 1;
}); });
afterEach(function (): void { afterEach(function () {
unset($this->state->bar); unset($this->state->bar);
}); });
it('does not get executed before the test', function (): void { it('does not get executed before the test', function () {
expect($this->state)->not->toHaveProperty('bar'); expect($this->state)->not->toHaveProperty('bar');
}); });
it('gets executed after the test', function (): void { it('gets executed after the test', function () {
expect($this->state)->toHaveProperty('bar') expect($this->state)->toHaveProperty('bar');
->and($this->state->bar)->toBe(2); expect($this->state->bar)->toBe(2);
}); });
afterEach(function (): void { afterEach(function () {
$this->state->bar = 2; $this->state->bar = 2;
}); });
describe('outer', function (): void { describe('outer', function () {
afterEach(function (): void { afterEach(function () {
$this->state->bar++; $this->state->bar++;
}); });
describe('inner', function (): void { describe('inner', function () {
afterEach(function (): void { afterEach(function () {
$this->state->bar++; $this->state->bar++;
}); });
it('does not get executed before the test', function (): void { it('does not get executed before the test', function () {
expect($this->state)->toHaveProperty('bar') expect($this->state)->toHaveProperty('bar');
->and($this->state->bar)->toBe(2); expect($this->state->bar)->toBe(2);
}); });
it('should call all parent afterEach functions', function (): void { it('should call all parent afterEach functions', function () {
expect($this->state)->toHaveProperty('bar') expect($this->state)->toHaveProperty('bar');
->and($this->state->bar)->toBe(4); expect($this->state->bar)->toBe(4);
}); });
}); });
}); });
describe('matching describe block names', function (): void { describe('matching describe block names', function () {
afterEach(function (): void { afterEach(function () {
$this->state->foo = 1; $this->state->foo = 1;
}); });
describe('outer', function (): void { describe('outer', function () {
afterEach(function (): void { afterEach(function () {
$this->state->foo++; $this->state->foo++;
}); });
describe('middle', function (): void { describe('middle', function () {
afterEach(function (): void { afterEach(function () {
$this->state->foo++; $this->state->foo++;
}); });
describe('inner', function (): void { describe('inner', function () {
afterEach(function (): void { afterEach(function () {
$this->state->foo++; $this->state->foo++;
}); });
it('does not get executed before the test', function (): void { it('does not get executed before the test', function () {
expect($this)->not->toHaveProperty('foo'); expect($this)->not->toHaveProperty('foo');
}); });
it('should call all parent afterEach functions', function (): void { it('should call all parent afterEach functions', function () {
expect($this->state->foo)->toBe(4); expect($this->state->foo)->toBe(4);
}); });
}); });
}); });
describe('middle', function (): void { describe('middle', function () {
it('does not get executed before the test', function (): void { it('does not get executed before the test', function () {
expect($this)->not->toHaveProperty('foo'); expect($this)->not->toHaveProperty('foo');
}); });
it('should not call afterEach functions for sibling describe blocks with the same name', function (): void { it('should not call afterEach functions for sibling describe blocks with the same name', function () {
expect($this)->not->toHaveProperty('foo'); expect($this)->not->toHaveProperty('foo');
}); });
}); });
describe('inner', function (): void { describe('inner', function () {
it('does not get executed before the test', function (): void { it('does not get executed before the test', function () {
expect($this)->not->toHaveProperty('foo'); expect($this)->not->toHaveProperty('foo');
}); });
it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function (): void { it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function () {
expect($this)->not->toHaveProperty('foo'); expect($this)->not->toHaveProperty('foo');
}); });
}); });
+4 -4
View File
@@ -1,15 +1,15 @@
<?php <?php
beforeEach(function (): void { beforeEach(function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->assignee('nunomaduro'); })->assignee('nunomaduro');
it('may be associated with an assignee', function (): void { it('may be associated with an assignee', function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->assignee('taylorotwell'); })->assignee('taylorotwell');
describe('nested', function (): void { describe('nested', function () {
it('may be associated with an assignee', function (): void { it('may be associated with an assignee', function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->assignee('taylorotwell'); })->assignee('taylorotwell');
})->assignee('nunomaduro')->note('an note between an the assignee')->assignee(['jamesbrooks', 'joedixon']); })->assignee('nunomaduro')->note('an note between an the assignee')->assignee(['jamesbrooks', 'joedixon']);
+3 -3
View File
@@ -3,16 +3,16 @@
$foo = new stdClass; $foo = new stdClass;
$foo->bar = 0; $foo->bar = 0;
beforeAll(function () use ($foo): void { beforeAll(function () use ($foo) {
$foo->bar++; $foo->bar++;
}); });
it('gets executed before tests', function () use ($foo): void { it('gets executed before tests', function () use ($foo) {
expect($foo)->bar->toBe(1); expect($foo)->bar->toBe(1);
$foo->bar = 'changed'; $foo->bar = 'changed';
}); });
it('do not get executed before each test', function () use ($foo): void { it('do not get executed before each test', function () use ($foo) {
expect($foo)->bar->toBe('changed'); expect($foo)->bar->toBe('changed');
}); });
+38 -38
View File
@@ -1,91 +1,91 @@
<?php <?php
beforeEach(function (): void { beforeEach(function () {
$this->bar = 2; $this->bar = 2;
}); });
beforeEach(function (): void { beforeEach(function () {
$this->bar++; $this->bar++;
}); });
beforeEach(function (): void { beforeEach(function () {
$this->bar = 0; $this->bar = 0;
}); });
it('gets executed before each test', function (): void { it('gets executed before each test', function () {
expect($this->bar)->toBe(1); expect($this->bar)->toBe(1);
$this->bar = 'changed'; $this->bar = 'changed';
}); });
it('gets executed before each test once again', function (): void { it('gets executed before each test once again', function () {
expect($this->bar)->toBe(1); expect($this->bar)->toBe(1);
}); });
beforeEach(function (): void { beforeEach(function () {
$this->bar++; $this->bar++;
}); });
describe('outer', function (): void { describe('outer', function () {
beforeEach(function (): void { beforeEach(function () {
$this->bar++; $this->bar++;
}); });
describe('inner', function (): void { describe('inner', function () {
beforeEach(function (): void { beforeEach(function () {
$this->bar++; $this->bar++;
}); });
it('should call all parent beforeEach functions', function (): void { it('should call all parent beforeEach functions', function () {
expect($this->bar)->toBe(3); expect($this->bar)->toBe(3);
}); });
}); });
}); });
describe('with expectations', function (): void { describe('with expectations', function () {
beforeEach()->expect(true)->toBeTrue(); beforeEach()->expect(true)->toBeTrue();
describe('nested block', function (): void { describe('nested block', function () {
test('test', function (): void {}); test('test', function () {});
}); });
test('test', function (): void {}); test('test', function () {});
}); });
describe('matching describe block names', function (): void { describe('matching describe block names', function () {
beforeEach(function (): void { beforeEach(function () {
$this->foo = 1; $this->foo = 1;
}); });
describe('outer', function (): void { describe('outer', function () {
beforeEach(function (): void { beforeEach(function () {
$this->foo++; $this->foo++;
}); });
describe('middle', function (): void { describe('middle', function () {
beforeEach(function (): void { beforeEach(function () {
$this->foo++; $this->foo++;
}); });
describe('inner', function (): void { describe('inner', function () {
beforeEach(function (): void { beforeEach(function () {
$this->foo++; $this->foo++;
}); });
it('should call all parent beforeEach functions', function (): void { it('should call all parent beforeEach functions', function () {
expect($this->foo)->toBe(4); expect($this->foo)->toBe(4);
}); });
}); });
}); });
describe('middle', function (): void { describe('middle', function () {
it('should not call beforeEach functions for sibling describe blocks with the same name', function (): void { it('should not call beforeEach functions for sibling describe blocks with the same name', function () {
expect($this->foo)->toBe(2); expect($this->foo)->toBe(2);
}); });
}); });
describe('inner', function (): void { describe('inner', function () {
it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function (): void { it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function () {
expect($this->foo)->toBe(2); expect($this->foo)->toBe(2);
}); });
}); });
@@ -93,36 +93,36 @@ describe('matching describe block names', function (): void {
}); });
$matchingNameCalls = 0; $matchingNameCalls = 0;
describe('matching name', function () use (&$matchingNameCalls): void { describe('matching name', function () use (&$matchingNameCalls) {
beforeEach(function () use (&$matchingNameCalls): void { beforeEach(function () use (&$matchingNameCalls) {
$matchingNameCalls++; $matchingNameCalls++;
}); });
it('should call the before each', function () use (&$matchingNameCalls): void { it('should call the before each', function () use (&$matchingNameCalls) {
expect($matchingNameCalls)->toBe(1); expect($matchingNameCalls)->toBe(1);
}); });
}); });
describe('matching name', function () use (&$matchingNameCalls): void { describe('matching name', function () use (&$matchingNameCalls) {
it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls): void { it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls) {
expect($matchingNameCalls)->toBe(1); expect($matchingNameCalls)->toBe(1);
}); });
}); });
beforeEach(function (): void { beforeEach(function () {
$this->baz = 1; $this->baz = 1;
}); });
describe('called on all tests', function (): void { describe('called on all tests', function () {
beforeEach(function (): void { beforeEach(function () {
$this->baz++; $this->baz++;
}); });
test('beforeEach should be called', function (): void { test('beforeEach should be called', function () {
expect($this->baz)->toBe(2); expect($this->baz)->toBe(2);
}); });
test('beforeEach should be called for all tests', function (): void { test('beforeEach should be called for all tests', function () {
expect($this->baz)->toBe(2); expect($this->baz)->toBe(2);
}); });
}); });
@@ -1,17 +1,15 @@
<?php <?php
declare(strict_types=1);
beforeEach()->expect(true)->toBeTrue(); beforeEach()->expect(true)->toBeTrue();
test('runs 1', function (): void { test('runs 1', function () {
// This test did performs assertions... // This test did performs assertions...
}); });
test('runs 2', function (): void { test('runs 2', function () {
// This test did performs assertions... // This test did performs assertions...
}); });
test('runs 3', function (): void { test('runs 3', function () {
// This test did performs assertions... // This test did performs assertions...
}); });
@@ -1,17 +1,15 @@
<?php <?php
declare(strict_types=1);
beforeEach()->skip(); beforeEach()->skip();
test('does not run 1', function (): void { test('does not run 1', function () {
$this->fail('This test should not run'); $this->fail('This test should not run');
}); });
test('does not run 2', function (): void { test('does not run 2', function () {
$this->fail('This test should not run'); $this->fail('This test should not run');
}); });
test('does not run 3', function (): void { test('does not run 3', function () {
$this->fail('This test should not run'); $this->fail('This test should not run');
}); });
@@ -2,11 +2,11 @@
beforeEach()->todo(); beforeEach()->todo();
test('is marked as todo 1', function (): void { test('is marked as todo 1', function () {
$this->fail('This test should not run'); $this->fail('This test should not run');
}); });
test('is marked as todo 2', function (): void { test('is marked as todo 2', function () {
$this->fail('This test should not run'); $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('has plugin')->assertTrue(class_exists(CoveragePlugin::class));
it('adds coverage if --coverage exist', function (): void { it('adds coverage if --coverage exist', function () {
$plugin = new CoveragePlugin(new ConsoleOutput); $plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverage)->toBeFalse(); expect($plugin->coverage)->toBeFalse();
$arguments = $plugin->handleArguments([]); $arguments = $plugin->handleArguments([]);
expect($arguments)->toBeEmpty() expect($arguments)->toEqual([])
->and($plugin->coverage)->toBeFalse(); ->and($plugin->coverage)->toBeFalse();
$arguments = $plugin->handleArguments(['--coverage']); $arguments = $plugin->handleArguments(['--coverage']);
@@ -19,7 +19,7 @@ it('adds coverage if --coverage exist', function (): void {
->and($plugin->coverage)->toBeTrue(); ->and($plugin->coverage)->toBeTrue();
})->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available'); })->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 (): void { it('adds coverage if --min exist', function () {
$plugin = new CoveragePlugin(new ConsoleOutput); $plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverageMin)->toEqual(0.0) expect($plugin->coverageMin)->toEqual(0.0)
->and($plugin->coverage)->toBeFalse(); ->and($plugin->coverage)->toBeFalse();
@@ -34,7 +34,7 @@ it('adds coverage if --min exist', function (): void {
expect($plugin->coverageMin)->toEqual(2.4); expect($plugin->coverageMin)->toEqual(2.4);
}); });
it('generates coverage based on file input', function (): void { it('generates coverage based on file input', function () {
expect(Coverage::getMissingCoverage(new class expect(Coverage::getMissingCoverage(new class
{ {
public function lineCoverageData(): array public function lineCoverageData(): array
+4 -4
View File
@@ -5,9 +5,9 @@ use Tests\Fixtures\Covers\CoversClass1;
covers([CoversClass1::class]); covers([CoversClass1::class]);
it('uses the correct PHPUnit attribute for class', function (): void { it('uses the correct PHPUnit attribute for class', function () {
$attributes = new ReflectionClass($this)->getAttributes(); $attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class) expect($attributes[1]->getName())->toBe(CoversClass::class);
->and($attributes[1]->getArguments()[0])->toBe(CoversClass1::class); expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1');
}); });
+1 -3
View File
@@ -1,11 +1,9 @@
<?php <?php
declare(strict_types=1);
use Pest\PendingCalls\TestCall; use Pest\PendingCalls\TestCall;
use Pest\TestSuite; use Pest\TestSuite;
it('throws exception if no class nor method has been found', function (): void { it('throws exception if no class nor method has been found', function () {
$testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure'); $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
$testCall->covers('fakeName'); $testCall->covers('fakeName');
+5 -5
View File
@@ -2,11 +2,11 @@
use PHPUnit\Framework\Attributes\CoversFunction; use PHPUnit\Framework\Attributes\CoversFunction;
function testCoversFunction(): void {} function testCoversFunction() {}
it('uses the correct PHPUnit attribute for function', function (): void { it('uses the correct PHPUnit attribute for function', function () {
$attributes = new ReflectionClass($this)->getAttributes(); $attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(CoversFunction::class) expect($attributes[1]->getName())->toBe(CoversFunction::class);
->and($attributes[1]->getArguments()[0])->toBe('testCoversFunction'); expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction');
})->coversFunction('testCoversFunction'); })->coversFunction('testCoversFunction');
+8 -7
View File
@@ -4,13 +4,14 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction; use PHPUnit\Framework\Attributes\CoversFunction;
use Tests\Fixtures\Covers\CoversClass3; use Tests\Fixtures\Covers\CoversClass3;
function testCoversFunction2(): void {} function testCoversFunction2() {}
it('guesses if the given argument is a class or function', function (): void { it('guesses if the given argument is a class or function', function () {
$attributes = new ReflectionClass($this)->getAttributes(); $attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class) expect($attributes[1]->getName())->toBe(CoversClass::class);
->and($attributes[1]->getArguments()[0])->toBe(CoversClass3::class) expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class);
->and($attributes[2]->getName())->toBe(CoversFunction::class)
->and($attributes[2]->getArguments()[0])->toBe('testCoversFunction2'); expect($attributes[2]->getName())->toBe(CoversFunction::class);
expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2');
})->covers(CoversClass3::class, 'testCoversFunction2'); })->covers(CoversClass3::class, 'testCoversFunction2');
+4 -4
View File
@@ -3,9 +3,9 @@
use PHPUnit\Framework\Attributes\CoversTrait as PHPUnitCoversTrait; use PHPUnit\Framework\Attributes\CoversTrait as PHPUnitCoversTrait;
use Tests\Fixtures\Covers\CoversTrait; use Tests\Fixtures\Covers\CoversTrait;
it('uses the correct PHPUnit attribute for trait', function (): void { it('uses the correct PHPUnit attribute for trait', function () {
$attributes = new ReflectionClass($this)->getAttributes(); $attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class) expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class);
->and($attributes[1]->getArguments()[0])->toBe(CoversTrait::class); expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait');
})->coversTrait(CoversTrait::class); })->coversTrait(CoversTrait::class);
+78 -78
View File
@@ -11,41 +11,41 @@
// beforeEach()->with() inside describe blocks // beforeEach()->with() inside describe blocks
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('beforeEach()->with() applies dataset to tests', function (): void { describe('beforeEach()->with() applies dataset to tests', function () {
beforeEach()->with([10]); beforeEach()->with([10]);
test('receives the dataset value', function ($value): void { test('receives the dataset value', function ($value) {
expect($value)->toBe(10); expect($value)->toBe(10);
}); });
it('also receives the dataset value in it()', function ($value): void { it('also receives the dataset value in it()', function ($value) {
expect($value)->toBe(10); expect($value)->toBe(10);
}); });
}); });
describe('beforeEach()->with() with multiple dataset values', function (): void { describe('beforeEach()->with() with multiple dataset values', function () {
beforeEach()->with([1, 2, 3]); beforeEach()->with([1, 2, 3]);
test('receives each value from the dataset', function ($value): void { test('receives each value from the dataset', function ($value) {
expect($value)->toBeIn([1, 2, 3]); expect($value)->toBeIn([1, 2, 3]);
}); });
}); });
describe('beforeEach()->with() with keyed dataset', function (): void { describe('beforeEach()->with() with keyed dataset', function () {
beforeEach()->with(['first' => [10], 'second' => [20]]); beforeEach()->with(['first' => [10], 'second' => [20]]);
test('receives keyed dataset values', function ($value): void { test('receives keyed dataset values', function ($value) {
expect($value)->toBeIn([10, 20]); expect($value)->toBeIn([10, 20]);
}); });
}); });
describe('beforeEach()->with() with closure dataset', function (): void { describe('beforeEach()->with() with closure dataset', function () {
beforeEach()->with(function () { beforeEach()->with(function () {
yield [100]; yield [100];
yield [200]; yield [200];
}); });
test('receives values from closure dataset', function ($value): void { test('receives values from closure dataset', function ($value) {
expect($value)->toBeIn([100, 200]); expect($value)->toBeIn([100, 200]);
}); });
}); });
@@ -54,30 +54,30 @@ describe('beforeEach()->with() with closure dataset', function (): void {
// describe()->with() method chaining // describe()->with() method chaining
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('describe()->with() passes dataset to tests', function (): void { describe('describe()->with() passes dataset to tests', function () {
test('receives the dataset value', function ($value): void { test('receives the dataset value', function ($value) {
expect($value)->toBe(42); expect($value)->toBe(42);
}); });
it('also receives it in it()', function ($value): void { it('also receives it in it()', function ($value) {
expect($value)->toBe(42); expect($value)->toBe(42);
}); });
})->with([42]); })->with([42]);
describe('describe()->with() with multiple values', function (): void { describe('describe()->with() with multiple values', function () {
test('receives each value', function ($value): void { test('receives each value', function ($value) {
expect($value)->toBeIn([5, 10, 15]); expect($value)->toBeIn([5, 10, 15]);
}); });
})->with([5, 10, 15]); })->with([5, 10, 15]);
describe('describe()->with() with keyed dataset', function (): void { describe('describe()->with() with keyed dataset', function () {
test('receives keyed values', function ($value): void { test('receives keyed values', function ($value) {
expect($value)->toBeIn([100, 200]); expect($value)->toBeIn([100, 200]);
}); });
})->with(['alpha' => [100], 'beta' => [200]]); })->with(['alpha' => [100], 'beta' => [200]]);
describe('describe()->with() with closure dataset', function (): void { describe('describe()->with() with closure dataset', function () {
test('receives closure dataset values', function ($value): void { test('receives closure dataset values', function ($value) {
expect($value)->toBeIn([7, 14]); expect($value)->toBeIn([7, 14]);
}); });
})->with(function () { })->with(function () {
@@ -89,33 +89,33 @@ describe('describe()->with() with closure dataset', function (): void {
// Nested describe blocks with datasets // Nested describe blocks with datasets
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('outer with dataset', function (): void { describe('outer with dataset', function () {
describe('inner without dataset', function (): void { describe('inner without dataset', function () {
test('inherits outer dataset', function (...$args): void { test('inherits outer dataset', function (...$args) {
expect($args)->toBe([1]); expect($args)->toBe([1]);
}); });
}); });
})->with([1]); })->with([1]);
describe('nested describe blocks with datasets at multiple levels', function (): void { describe('nested describe blocks with datasets at multiple levels', function () {
describe('level 1', function (): void { describe('level 1', function () {
test('receives level 1 dataset', function (...$args): void { test('receives level 1 dataset', function (...$args) {
expect($args)->toBe([10]); expect($args)->toBe([10]);
}); });
describe('level 2', function (): void { describe('level 2', function () {
test('receives datasets from all ancestor levels', function (...$args): void { test('receives datasets from all ancestor levels', function (...$args) {
expect($args)->toBe([10, 20]); expect($args)->toBe([10, 20]);
}); });
})->with([20]); })->with([20]);
})->with([10]); })->with([10]);
}); });
describe('deeply nested describe with datasets', function (): void { describe('deeply nested describe with datasets', function () {
describe('a', function (): void { describe('a', function () {
describe('b', function (): void { describe('b', function () {
describe('c', function (): void { describe('c', function () {
test('receives all ancestor datasets', function (...$args): void { test('receives all ancestor datasets', function (...$args) {
expect($args)->toBe([1, 2, 3]); expect($args)->toBe([1, 2, 3]);
}); });
})->with([3]); })->with([3]);
@@ -127,19 +127,19 @@ describe('deeply nested describe with datasets', function (): void {
// Combining hook datasets with test-level datasets // Combining hook datasets with test-level datasets
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('beforeEach()->with() combined with test->with()', function (): void { describe('beforeEach()->with() combined with test->with()', function () {
beforeEach()->with([10]); beforeEach()->with([10]);
test('receives both datasets as cross product', function ($hookValue, $testValue): void { test('receives both datasets as cross product', function ($hookValue, $testValue) {
expect($hookValue)->toBe(10) expect($hookValue)->toBe(10);
->and($testValue)->toBeIn([1, 2]); expect($testValue)->toBeIn([1, 2]);
})->with([1, 2]); })->with([1, 2]);
}); });
describe('describe()->with() combined with test->with()', function (): void { describe('describe()->with() combined with test->with()', function () {
test('receives both datasets', function ($describeValue, $testValue): void { test('receives both datasets', function ($describeValue, $testValue) {
expect($describeValue)->toBe(5) expect($describeValue)->toBe(5);
->and($testValue)->toBeIn([50, 60]); expect($testValue)->toBeIn([50, 60]);
})->with([50, 60]); })->with([50, 60]);
})->with([5]); })->with([5]);
@@ -147,33 +147,33 @@ describe('describe()->with() combined with test->with()', function (): void {
// beforeEach()->with() combined with beforeEach closure // beforeEach()->with() combined with beforeEach closure
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('beforeEach closure and beforeEach()->with() coexist', function (): void { describe('beforeEach closure and beforeEach()->with() coexist', function () {
beforeEach(function (): void { beforeEach(function () {
$this->setupValue = 'initialized'; $this->setupValue = 'initialized';
}); });
beforeEach()->with([99]); beforeEach()->with([99]);
test('has both the closure state and dataset', function ($value): void { test('has both the closure state and dataset', function ($value) {
expect($this->setupValue)->toBe('initialized') expect($this->setupValue)->toBe('initialized');
->and($value)->toBe(99); expect($value)->toBe(99);
}); });
}); });
describe('beforeEach()->with() does not interfere with closure hooks', function (): void { describe('beforeEach()->with() does not interfere with closure hooks', function () {
beforeEach(function (): void { beforeEach(function () {
$this->counter = 1; $this->counter = 1;
}); });
beforeEach(function (): void { beforeEach(function () {
$this->counter++; $this->counter++;
}); });
beforeEach()->with([42]); beforeEach()->with([42]);
test('closures run in order and dataset is applied', function ($value): void { test('closures run in order and dataset is applied', function ($value) {
expect($this->counter)->toBe(2) expect($this->counter)->toBe(2);
->and($value)->toBe(42); expect($value)->toBe(42);
}); });
}); });
@@ -181,24 +181,24 @@ describe('beforeEach()->with() does not interfere with closure hooks', function
// Dataset isolation between describe blocks // Dataset isolation between describe blocks
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('first describe with dataset', function (): void { describe('first describe with dataset', function () {
beforeEach()->with([111]); beforeEach()->with([111]);
test('gets its own dataset', function ($value): void { test('gets its own dataset', function ($value) {
expect($value)->toBe(111); expect($value)->toBe(111);
}); });
}); });
describe('second describe with different dataset', function (): void { describe('second describe with different dataset', function () {
beforeEach()->with([222]); beforeEach()->with([222]);
test('gets its own dataset, not the sibling', function ($value): void { test('gets its own dataset, not the sibling', function ($value) {
expect($value)->toBe(222); expect($value)->toBe(222);
}); });
}); });
describe('third describe without dataset', function (): void { describe('third describe without dataset', function () {
test('has no dataset leaking from siblings', function (): void { test('has no dataset leaking from siblings', function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
}); });
}); });
@@ -207,23 +207,23 @@ describe('third describe without dataset', function (): void {
// describe()->with() combined with beforeEach hooks // describe()->with() combined with beforeEach hooks
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('describe()->with() with beforeEach closure', function (): void { describe('describe()->with() with beforeEach closure', function () {
beforeEach(function (): void { beforeEach(function () {
$this->hookRan = true; $this->hookRan = true;
}); });
test('both hook and dataset work', function ($value): void { test('both hook and dataset work', function ($value) {
expect($this->hookRan)->toBeTrue() expect($this->hookRan)->toBeTrue();
->and($value)->toBe(77); expect($value)->toBe(77);
}); });
})->with([77]); })->with([77]);
describe('describe()->with() with afterEach closure', function (): void { describe('describe()->with() with afterEach closure', function () {
afterEach(function (): void { afterEach(function () {
expect($this->value)->toBe(88); expect($this->value)->toBe(88);
}); });
test('dataset is available and afterEach runs', function ($value): void { test('dataset is available and afterEach runs', function ($value) {
$this->value = $value; $this->value = $value;
expect($value)->toBe(88); expect($value)->toBe(88);
}); });
@@ -233,18 +233,18 @@ describe('describe()->with() with afterEach closure', function (): void {
// Multiple tests in a describe with beforeEach()->with() // Multiple tests in a describe with beforeEach()->with()
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('multiple tests share the same beforeEach dataset', function (): void { describe('multiple tests share the same beforeEach dataset', function () {
beforeEach()->with([33]); beforeEach()->with([33]);
test('first test gets the dataset', function ($value): void { test('first test gets the dataset', function ($value) {
expect($value)->toBe(33); expect($value)->toBe(33);
}); });
test('second test also gets the dataset', function ($value): void { test('second test also gets the dataset', function ($value) {
expect($value)->toBe(33); expect($value)->toBe(33);
}); });
it('third test with it() also gets the dataset', function ($value): void { it('third test with it() also gets the dataset', function ($value) {
expect($value)->toBe(33); expect($value)->toBe(33);
}); });
}); });
@@ -253,21 +253,21 @@ describe('multiple tests share the same beforeEach dataset', function (): void {
// Nested describe with beforeEach()->with() at inner level // Nested describe with beforeEach()->with() at inner level
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('outer describe', function (): void { describe('outer describe', function () {
beforeEach(function (): void { beforeEach(function () {
$this->outer = true; $this->outer = true;
}); });
describe('inner describe with dataset on hook', function (): void { describe('inner describe with dataset on hook', function () {
beforeEach()->with([55]); beforeEach()->with([55]);
test('inherits outer beforeEach and has inner dataset', function ($value): void { test('inherits outer beforeEach and has inner dataset', function ($value) {
expect($this->outer)->toBeTrue() expect($this->outer)->toBeTrue();
->and($value)->toBe(55); expect($value)->toBe(55);
}); });
}); });
test('outer test is unaffected by inner dataset', function (): void { test('outer test is unaffected by inner dataset', function () {
expect($this->outer)->toBeTrue(); expect($this->outer)->toBeTrue();
}); });
}); });
@@ -276,12 +276,12 @@ describe('outer describe', function (): void {
// describe()->with() with depends // describe()->with() with depends
// --------------------------------------------------------------- // ---------------------------------------------------------------
describe('describe()->with() preserves depends', function (): void { describe('describe()->with() preserves depends', function () {
test('first', function ($value): void { test('first', function ($value) {
expect($value)->toBe(9); expect($value)->toBe(9);
}); });
test('second', function ($value): void { test('second', function ($value) {
expect($value)->toBe(9); expect($value)->toBe(9);
})->depends('first'); })->depends('first');
})->with([9]); })->with([9]);
-46
View File
@@ -1,46 +0,0 @@
<?php
use Symfony\Component\Process\Process;
$run = function (string $target): array {
$process = new Process(
['php', 'bin/pest', $target],
dirname(__DIR__, 2),
['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1'],
);
$process->run();
return [
'output' => removeAnsiEscapeSequences($process->getOutput().$process->getErrorOutput()),
'code' => $process->getExitCode(),
];
};
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')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
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')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
test('reports dataset closure exceptions as errors', function () use ($run): void {
$result = $run('tests/.tests/DatasetClosureThrows.php');
expect($result['output'])
->toContain('boom from dataset')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
+189 -142
View File
@@ -5,20 +5,25 @@ use Pest\Exceptions\DatasetDoesNotExist;
use Pest\Plugin; use Pest\Plugin;
use Pest\Repositories\DatasetsRepository; use Pest\Repositories\DatasetsRepository;
beforeEach(function (): void { beforeEach(function () {
$this->foo = 'bar'; $this->foo = 'bar';
}); });
it('throws exception if dataset does not exist', function (): void { it('throws exception if dataset does not exist', function () {
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']);`."); $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 already exist', function (): void { 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__.'].');
DatasetsRepository::set('second', [[]], __DIR__); DatasetsRepository::set('second', [[]], __DIR__);
expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset with the name `second` already exists in scope ['.__DIR__.'].');
}); });
it('sets closures', function (): void { it('sets closures', function () {
DatasetsRepository::set('foo', function () { DatasetsRepository::set('foo', function () {
yield [1]; yield [1];
}, __DIR__); }, __DIR__);
@@ -26,18 +31,18 @@ it('sets closures', function (): void {
expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]); expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]);
}); });
it('sets arrays', function (): void { it('sets arrays', function () {
DatasetsRepository::set('bar', [[2]], __DIR__); DatasetsRepository::set('bar', [[2]], __DIR__);
expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]); expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]);
}); });
it('gets bound to test case object', function ($value): void { it('gets bound to test case object', function ($value) {
expect(true)->toBeTrue(); $this->assertTrue(true);
})->with([['a'], ['b']]); })->with([['a'], ['b']]);
test('it truncates the description', function (): void { test('it truncates the description', function () {
expect(true)->toBeTrue(); expect(true)->toBe(true);
// it gets tested by the integration test // it gets tested by the integration test
})->with([str_repeat('Fooo', 10)]); })->with([str_repeat('Fooo', 10)]);
@@ -46,59 +51,61 @@ $state->text = '';
$datasets = [[1], [2]]; $datasets = [[1], [2]];
test('lazy datasets', function ($text) use ($state, $datasets): void { test('lazy datasets', function ($text) use ($state, $datasets) {
$state->text .= $text; $state->text .= $text;
expect([$text])->toBeIn($datasets); expect(in_array([$text], $datasets))->toBe(true);
})->with($datasets); })->with($datasets);
test('lazy datasets did the job right', function () use ($state): void { test('lazy datasets did the job right', function () use ($state) {
expect($state->text)->toBe('12'); expect($state->text)->toBe('12');
}); });
test('interpolated :dataset lazy datasets', function ($text): void { test('interpolated :dataset lazy datasets', function ($text) {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->with($datasets); })->with($datasets);
$state->text = ''; $state->text = '';
test('eager datasets', function ($text) use ($state, $datasets): void { test('eager datasets', function ($text) use ($state, $datasets) {
$state->text .= $text; $state->text .= $text;
expect($datasets)->toContain([$text]); expect($datasets)->toContain([$text]);
})->with(fn (): array => $datasets); })->with(function () use ($datasets) {
return $datasets;
});
test('eager datasets did the job right', function () use ($state): void { test('eager datasets did the job right', function () use ($state) {
expect($state->text)->toBe('1212'); expect($state->text)->toBe('1212');
}); });
test('lazy registered datasets', function ($text) use ($state, $datasets): void { test('lazy registered datasets', function ($text) use ($state, $datasets) {
$state->text .= $text; $state->text .= $text;
expect($datasets)->toContain([$text]); expect($datasets)->toContain([$text]);
})->with('numbers.array'); })->with('numbers.array');
test('lazy registered datasets did the job right', function () use ($state): void { test('lazy registered datasets did the job right', function () use ($state) {
expect($state->text)->toBe('121212'); expect($state->text)->toBe('121212');
}); });
test('eager registered datasets', function ($text) use ($state, $datasets): void { test('eager registered datasets', function ($text) use ($state, $datasets) {
$state->text .= $text; $state->text .= $text;
expect($datasets)->toContain([$text]); expect($datasets)->toContain([$text]);
})->with('numbers.closure'); })->with('numbers.closure');
test('eager registered datasets did the job right', function () use ($state): void { test('eager registered datasets did the job right', function () use ($state) {
expect($state->text)->toBe('12121212'); expect($state->text)->toBe('12121212');
}); });
test('eager wrapped registered datasets', function ($text) use ($state, $datasets): void { test('eager wrapped registered datasets', function ($text) use ($state, $datasets) {
$state->text .= $text; $state->text .= $text;
expect($datasets)->toContain([$text]); expect($datasets)->toContain([$text]);
})->with('numbers.closure.wrapped'); })->with('numbers.closure.wrapped');
test('eager registered wrapped datasets did the job right', function () use ($state): void { test('eager registered wrapped datasets did the job right', function () use ($state) {
expect($state->text)->toBe('1212121212'); expect($state->text)->toBe('1212121212');
}); });
test('named datasets', function ($text) use ($state, $datasets): void { test('named datasets', function ($text) use ($state, $datasets) {
$state->text .= $text; $state->text .= $text;
expect($datasets)->toContain([$text]); expect($datasets)->toContain([$text]);
})->with([ })->with([
@@ -106,14 +113,14 @@ test('named datasets', function ($text) use ($state, $datasets): void {
'two' => [2], 'two' => [2],
]); ]);
test('interpolated :dataset named datasets', function ($text): void { test('interpolated :dataset named datasets', function ($text) {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->with([ })->with([
'one' => [1], 'one' => [1],
'two' => [2], 'two' => [2],
]); ]);
test('named datasets did the job right', function () use ($state): void { test('named datasets did the job right', function () use ($state) {
expect($state->text)->toBe('121212121212'); expect($state->text)->toBe('121212121212');
}); });
@@ -126,13 +133,13 @@ $namedDatasets = [
new Bar, new Bar,
]; ];
test('lazy named datasets', function ($text): void { test('lazy named datasets', function ($text) {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->with($namedDatasets); })->with($namedDatasets);
$counter = 0; $counter = 0;
it('creates unique test case names', function (string $name, Plugin $plugin, bool $bool) use (&$counter): void { it('creates unique test case names', function (string $name, Plugin $plugin, bool $bool) use (&$counter) {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
$counter++; $counter++;
})->with([ })->with([
@@ -144,69 +151,73 @@ it('creates unique test case names', function (string $name, Plugin $plugin, boo
['Name 1', new Plugin, true], ['Name 1', new Plugin, true],
]); ]);
it('creates unique test case names - count', function () use (&$counter): void { it('creates unique test case names - count', function () use (&$counter) {
expect($counter)->toBe(6); expect($counter)->toBe(6);
}); });
$datasets_a = [[1], [2]]; $datasets_a = [[1], [2]];
$datasets_b = [[3], [4]]; $datasets_b = [[3], [4]];
test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void { test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
$state->text .= $text_a.$text_b; $state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]) expect($datasets_a)->toContain([$text_a]);
->and($datasets_b)->toContain([$text_b]); expect($datasets_b)->toContain([$text_b]);
})->with($datasets_a, $datasets_b); })->with($datasets_a, $datasets_b);
test('lazy multiple datasets did the job right', function () use ($state): void { test('lazy multiple datasets did the job right', function () use ($state) {
expect($state->text)->toBe('12121212121213142324'); expect($state->text)->toBe('12121212121213142324');
}); });
$state->text = ''; $state->text = '';
test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void { test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
$state->text .= $text_a.$text_b; $state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]) expect($datasets_a)->toContain([$text_a]);
->and($datasets_b)->toContain([$text_b]); expect($datasets_b)->toContain([$text_b]);
})->with(fn (): array => $datasets_a)->with(fn (): array => $datasets_b); })->with(function () use ($datasets_a) {
return $datasets_a;
})->with(function () use ($datasets_b) {
return $datasets_b;
});
test('eager multiple datasets did the job right', function () use ($state): void { test('eager multiple datasets did the job right', function () use ($state) {
expect($state->text)->toBe('1212121212121314232413142324'); expect($state->text)->toBe('1212121212121314232413142324');
}); });
test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void { test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
$state->text .= $text_a.$text_b; $state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]) expect($datasets)->toContain([$text_a]);
->toContain([$text_b]); expect($datasets)->toContain([$text_b]);
})->with('numbers.array')->with('numbers.array'); })->with('numbers.array')->with('numbers.array');
test('lazy registered multiple datasets did the job right', function () use ($state): void { test('lazy registered multiple datasets did the job right', function () use ($state) {
expect($state->text)->toBe('121212121212131423241314232411122122'); expect($state->text)->toBe('121212121212131423241314232411122122');
}); });
test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void { test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
$state->text .= $text_a.$text_b; $state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]) expect($datasets)->toContain([$text_a]);
->toContain([$text_b]); expect($datasets)->toContain([$text_b]);
})->with('numbers.array')->with('numbers.closure'); })->with('numbers.array')->with('numbers.closure');
test('eager registered multiple datasets did the job right', function () use ($state): void { test('eager registered multiple datasets did the job right', function () use ($state) {
expect($state->text)->toBe('12121212121213142324131423241112212211122122'); expect($state->text)->toBe('12121212121213142324131423241112212211122122');
}); });
test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void { test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
$state->text .= $text_a.$text_b; $state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]) expect($datasets)->toContain([$text_a]);
->toContain([$text_b]); expect($datasets)->toContain([$text_b]);
})->with('numbers.closure.wrapped')->with('numbers.closure'); })->with('numbers.closure.wrapped')->with('numbers.closure');
test('eager wrapped registered multiple datasets did the job right', function () use ($state): void { test('eager wrapped registered multiple datasets did the job right', function () use ($state) {
expect($state->text)->toBe('1212121212121314232413142324111221221112212211122122'); expect($state->text)->toBe('1212121212121314232413142324111221221112212211122122');
}); });
test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void { test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
$state->text .= $text_a.$text_b; $state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]) expect($datasets_a)->toContain([$text_a]);
->and($datasets_b)->toContain([$text_b]); expect($datasets_b)->toContain([$text_b]);
})->with([ })->with([
'one' => [1], 'one' => [1],
'two' => [2], 'two' => [2],
@@ -215,18 +226,18 @@ test('named multiple datasets', function ($text_a, $text_b) use ($state, $datase
'four' => [4], 'four' => [4],
]); ]);
test('named multiple datasets did the job right', function () use ($state): void { test('named multiple datasets did the job right', function () use ($state) {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324'); expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324');
}); });
test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b): void { test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b) {
$state->text .= $text_a.$text_b.$text_c; $state->text .= $text_a.$text_b.$text_c;
expect($datasets_a)->toContain([$text_a]) expect($datasets_a)->toContain([$text_a]);
->and($datasets_b)->toContain([$text_b]) expect($datasets_b)->toContain([$text_b]);
->and([5, 6])->toContain($text_c); expect([5, 6])->toContain($text_c);
})->with($datasets_a, $datasets_b)->with([5, 6]); })->with($datasets_a, $datasets_b)->with([5, 6]);
test('more than two datasets did the job right', function () use ($state): void { test('more than two datasets did the job right', function () use ($state) {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246'); expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246');
}); });
@@ -239,97 +250,125 @@ test(
function (int $text) use ( function (int $text) use (
$wrapped_generator_state, $wrapped_generator_state,
$wrapped_generator_function_datasets $wrapped_generator_function_datasets
): void { ) {
$wrapped_generator_state->text .= $text; $wrapped_generator_state->text .= $text;
expect($text)->toBeIn($wrapped_generator_function_datasets); expect(in_array($text, $wrapped_generator_function_datasets))->toBe(true);
} }
)->with('numbers.generators.wrapped'); )->with('numbers.generators.wrapped');
test('eager registered wrapped datasets with Generator functions did the job right', function () use ($wrapped_generator_state): void { test('eager registered wrapped datasets with Generator functions did the job right', function () use ($wrapped_generator_state) {
expect($wrapped_generator_state->text)->toBe('1234'); expect($wrapped_generator_state->text)->toBe('1234');
}); });
test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description): void { test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description) {
expect($wrapped_generator_state_with_description)->not->toBeEmpty(); expect($wrapped_generator_state_with_description)->not->toBeEmpty();
})->with(function () { })->with(function () {
yield 'taylor' => 'taylor@laravel.com'; yield 'taylor' => 'taylor@laravel.com';
yield 'james' => 'james@laravel.com'; yield 'james' => 'james@laravel.com';
}); });
it('can resolve a dataset after the test case is available', function ($result): void { it('can resolve a dataset after the test case is available', function ($result) {
expect($result)->toBe('bar'); expect($result)->toBe('bar');
})->with([ })->with([
fn () => $this->foo, function () {
return $this->foo;
},
[ [
fn () => $this->foo, function () {
return $this->foo;
},
], ],
]); ]);
it('can resolve a dataset after the test case is available with multiple datasets', function (string $result, string $result2): void { it('can resolve a dataset after the test case is available with multiple datasets', function (string $result, string $result2) {
expect($result)->toBe('bar'); expect($result)->toBe('bar');
})->with([ })->with([
fn () => $this->foo, 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;
},
], ],
]); ]);
it('can resolve a dataset after the test case is available with shared yield sets', function ($result): void { it('can resolve a dataset after the test case is available with shared yield sets', function ($result) {
expect($result)->toBeInt()->toBeLessThan(3); expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.closure'); })->with('bound.closure');
it('can resolve a dataset after the test case is available with shared array sets', function ($result): void { it('can resolve a dataset after the test case is available with shared array sets', function ($result) {
expect($result)->toBeInt()->toBeLessThan(3); expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.array'); })->with('bound.array');
it('resolves a potential bound dataset logically', function ($foo, $bar): void { it('resolves a potential bound dataset logically', function ($foo, $bar) {
expect($foo)->toBe('foo') expect($foo)->toBe('foo');
->and($bar())->toBe('bar'); expect($bar())->toBe('bar');
})->with([ })->with([
[ [
'foo', 'foo',
fn (): string => 'bar', function () {
return 'bar';
},
], // This should be passed as a closure because we've passed multiple arguments ], // 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): void { it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar) {
expect($foo())->toBe('foo') expect($foo())->toBe('foo');
->and($bar)->toBe('bar'); expect($bar)->toBe('bar');
})->with([ })->with([
[ [
fn (): string => 'foo', 'bar', function () {
return 'foo';
}, 'bar',
], // This should be passed as a closure because we've passed multiple arguments ], // 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): void { it('will not resolve a closure if it is type hinted as a closure', function (Closure $data) {
expect($data())->toBeString(); expect($data())->toBeString();
})->with([ })->with([
fn (): string => 'foo', function () {
fn (): string => 'bar', return 'foo';
},
function () {
return 'bar';
},
]); ]);
it('will not resolve a closure if it is type hinted as a callable', function (callable $data): void { it('will not resolve a closure if it is type hinted as a callable', function (callable $data) {
expect($data())->toBeString(); expect($data())->toBeString();
})->with([ })->with([
fn (): string => 'foo', function () {
fn (): string => 'bar', return 'foo';
},
function () {
return 'bar';
},
]); ]);
it('can correctly resolve a bound dataset that returns an array', function (array $data): void { it('can correctly resolve a bound dataset that returns an array', function (array $data) {
expect($data)->toBe(['foo', 'bar', 'baz']); expect($data)->toBe(['foo', 'bar', 'baz']);
})->with([ })->with([
fn (): array => ['foo', 'bar', 'baz'], function () {
return ['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): void { it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz) {
expect([$foo, $bar, $baz])->toBe(['foo', 'bar', 'baz']); expect([$foo, $bar, $baz])->toBe(['foo', 'bar', 'baz']);
})->with([ })->with([
fn (): array => ['foo', 'bar', 'baz'], function () {
return ['foo', 'bar', 'baz'];
},
]); ]);
todo('forbids to define tests in Datasets dirs and Datasets.php files'); todo('forbids to define tests in Datasets dirs and Datasets.php files');
@@ -354,57 +393,57 @@ it('may be used with high order even when bound')
->expect(fn (string $greeting) => $greeting) ->expect(fn (string $greeting) => $greeting)
->throws(InvalidArgumentException::class); ->throws(InvalidArgumentException::class);
describe('with on nested describe', function (): void { describe('with on nested describe', function () {
describe('nested', function (): void { describe('nested', function () {
test('before inner describe block', function (...$args): void { test('before inner describe block', function (...$args) {
expect($args)->toBe([1]); expect($args)->toBe([1]);
}); });
describe('describe', function (): void { describe('describe', function () {
it('should include the with value from all parent describe blocks', function (...$args): void { it('should include the with value from all parent describe blocks', function (...$args) {
expect($args)->toBe([1, 2]); expect($args)->toBe([1, 2]);
}); });
test('should include the with value from all parent describe blocks and the test', function (...$args): void { test('should include the with value from all parent describe blocks and the test', function (...$args) {
expect($args)->toBe([1, 2, 3]); expect($args)->toBe([1, 2, 3]);
})->with([3]); })->with([3]);
})->with([2]); })->with([2]);
test('after inner describe block', function (...$args): void { test('after inner describe block', function (...$args) {
expect($args)->toBe([1]); expect($args)->toBe([1]);
}); });
})->with([1]); })->with([1]);
}); });
describe('matching describe block names', function (): void { describe('matching describe block names', function () {
describe('outer', function (): void { describe('outer', function () {
test('before inner describe block', function (...$args): void { test('before inner describe block', function (...$args) {
expect($args)->toBe([1]); expect($args)->toBe([1]);
}); });
describe('inner', function (): void { describe('inner', function () {
it('should include the with value from all parent describe blocks', function (...$args): void { it('should include the with value from all parent describe blocks', function (...$args) {
expect($args)->toBe([1, 2]); expect($args)->toBe([1, 2]);
}); });
test('should include the with value from all parent describe blocks and the test', function (...$args): void { test('should include the with value from all parent describe blocks and the test', function (...$args) {
expect($args)->toBe([1, 2, 3]); expect($args)->toBe([1, 2, 3]);
})->with([3]); })->with([3]);
})->with([2]); })->with([2]);
describe('inner', function (): void { describe('inner', function () {
it('should not include the value from the other describe block with the same name', function (...$args): void { it('should not include the value from the other describe block with the same name', function (...$args) {
expect($args)->toBe([1]); expect($args)->toBe([1]);
}); });
}); });
test('after inner describe block', function (...$args): void { test('after inner describe block', function (...$args) {
expect($args)->toBe([1]); expect($args)->toBe([1]);
}); });
})->with([1]); })->with([1]);
}); });
test('after describe block', function (...$args): void { test('after describe block', function (...$args) {
expect($args)->toBe([5]); expect($args)->toBe([5]);
})->with([5]); })->with([5]);
@@ -415,58 +454,64 @@ it('may be used with high order after describe block')
dataset('after-describe', ['after']); dataset('after-describe', ['after']);
test('after describe block with named dataset', function (...$args): void { test('after describe block with named dataset', function (...$args) {
expect($args)->toBe(['after']); expect($args)->toBe(['after']);
})->with('after-describe'); })->with('after-describe');
test('named parameters match by parameter name', function (string $email, string $name): void { test('named parameters match by parameter name', function (string $email, string $name) {
expect($name)->toBe('Taylor') expect($name)->toBe('Taylor');
->and($email)->toBe('taylor@laravel.com'); expect($email)->toBe('taylor@laravel.com');
})->with([ })->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'], ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
]); ]);
test('named parameters work with multiple dataset items', function (string $email, string $name): void { test('named parameters work with multiple dataset items', function (string $email, string $name) {
expect($name)->toBeString() expect($name)->toBeString();
->and($email)->toContain('@'); expect($email)->toContain('@');
})->with([ })->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'], ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
['name' => 'James', 'email' => 'james@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): void { test('named parameters work in different order than closure params', function (string $third, string $first, string $second) {
expect($first)->toBe('a') expect($first)->toBe('a');
->and($second)->toBe('b') expect($second)->toBe('b');
->and($third)->toBe('c'); expect($third)->toBe('c');
})->with([ })->with([
['first' => 'a', 'second' => 'b', 'third' => 'c'], ['first' => 'a', 'second' => 'b', 'third' => 'c'],
]); ]);
test('named parameters work with named dataset keys', function (string $email, string $name): void { test('named parameters work with named dataset keys', function (string $email, string $name) {
expect($name)->toBeString() expect($name)->toBeString();
->and($email)->toContain('@'); expect($email)->toContain('@');
})->with([ })->with([
'taylor' => ['name' => 'Taylor', 'email' => 'taylor@laravel.com'], 'taylor' => ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
'james' => ['name' => 'James', 'email' => 'james@laravel.com'], 'james' => ['name' => 'James', 'email' => 'james@laravel.com'],
]); ]);
test('named parameters work with closures that should be resolved', function (string $email, string $name): void { test('named parameters work with closures that should be resolved', function (string $email, string $name) {
expect($name)->toBe('bar') expect($name)->toBe('bar');
->and($email)->toBe('bar@example.com'); expect($email)->toBe('bar@example.com');
})->with([ })->with([
[ [
'name' => fn () => $this->foo, 'name' => function () {
'email' => fn (): string => $this->foo.'@example.com', return $this->foo;
},
'email' => function () {
return $this->foo.'@example.com';
},
], ],
]); ]);
test('named parameters work with closure type hints', function (Closure $callback, string $name): void { test('named parameters work with closure type hints', function (Closure $callback, string $name) {
expect($name)->toBe('Taylor') expect($name)->toBe('Taylor');
->and($callback())->toBe('resolved'); expect($callback())->toBe('resolved');
})->with([ })->with([
[ [
'name' => 'Taylor', 'name' => 'Taylor',
'callback' => fn (): string => 'resolved', 'callback' => function () {
return 'resolved';
},
], ],
]); ]);
@@ -475,21 +520,23 @@ dataset('named-params-dataset', [
['name' => 'James', 'email' => 'james@laravel.com'], ['name' => 'James', 'email' => 'james@laravel.com'],
]); ]);
test('named parameters work with registered datasets', function (string $email, string $name): void { test('named parameters work with registered datasets', function (string $email, string $name) {
expect($name)->toBeString() expect($name)->toBeString();
->and($email)->toContain('@'); expect($email)->toContain('@');
})->with('named-params-dataset'); })->with('named-params-dataset');
test('named parameters work with bound closure returning associative array', function (string $email, string $name): void { test('named parameters work with bound closure returning associative array', function (string $email, string $name) {
expect($name)->toBe('bar') expect($name)->toBe('bar');
->and($email)->toBe('test@example.com'); expect($email)->toBe('test@example.com');
})->with([ })->with([
fn (): array => ['name' => $this->foo, 'email' => 'test@example.com'], function () {
return ['name' => $this->foo, 'email' => 'test@example.com'];
},
]); ]);
test('dataset items can mix named and sequential styles', function (string $name, string $email): void { test('dataset items can mix named and sequential styles', function (string $name, string $email) {
expect($name)->toBeString() expect($name)->toBeString();
->and($email)->toContain('@'); expect($email)->toContain('@');
})->with([ })->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'], ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
['James', 'james@laravel.com'], ['James', 'james@laravel.com'],
+17 -17
View File
@@ -2,34 +2,34 @@
$runCounter = 0; $runCounter = 0;
test('first', function () use (&$runCounter): string { test('first', function () use (&$runCounter) {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
$runCounter++; $runCounter++;
return 'first'; return 'first';
}); });
test('second', function () use (&$runCounter): string { test('second', function () use (&$runCounter) {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
$runCounter++; $runCounter++;
return 'second'; return 'second';
}); });
test('depends', function (): void { test('depends', function () {
expect(func_get_args())->toBe(['first', 'second']); expect(func_get_args())->toBe(['first', 'second']);
})->depends('first', 'second'); })->depends('first', 'second');
test('depends with ...params', function (string ...$params): void { test('depends with ...params', function (string ...$params) {
expect(func_get_args())->toBe($params); expect(func_get_args())->toBe($params);
})->depends('first', 'second'); })->depends('first', 'second');
test('depends with defined arguments', function (string $first, string $second): void { test('depends with defined arguments', function (string $first, string $second) {
expect($first)->toBe('first') expect($first)->toBe('first');
->and($second)->toBe('second'); expect($second)->toBe('second');
})->depends('first', 'second'); })->depends('first', 'second');
test('depends run test only once', function () use (&$runCounter): void { test('depends run test only once', function () use (&$runCounter) {
expect($runCounter)->toBe(2); expect($runCounter)->toBe(2);
})->depends('first', 'second'); })->depends('first', 'second');
@@ -37,42 +37,42 @@ test('depends run test only once', function () use (&$runCounter): void {
it('asserts true is true')->assertTrue(true); it('asserts true is true')->assertTrue(true);
test('depends works with the correct test name')->assertTrue(true)->depends('it asserts true is true'); test('depends works with the correct test name')->assertTrue(true)->depends('it asserts true is true');
describe('describe block', function (): void { describe('describe block', function () {
$runCounter = 0; $runCounter = 0;
test('first in describe', function () use (&$runCounter): void { test('first in describe', function () use (&$runCounter) {
$runCounter++; $runCounter++;
expect(true)->toBeTrue(); expect(true)->toBeTrue();
}); });
test('second in describe', function () use (&$runCounter): void { test('second in describe', function () use (&$runCounter) {
expect($runCounter)->toBe(1); expect($runCounter)->toBe(1);
$runCounter++; $runCounter++;
})->depends('first in describe'); })->depends('first in describe');
test('third in describe', function () use (&$runCounter): void { test('third in describe', function () use (&$runCounter) {
expect($runCounter)->toBe(2); expect($runCounter)->toBe(2);
})->depends('second in describe'); })->depends('second in describe');
describe('nested describe', function (): void { describe('nested describe', function () {
$runCounter = 0; $runCounter = 0;
test('first in nested describe', function () use (&$runCounter): void { test('first in nested describe', function () use (&$runCounter) {
$runCounter++; $runCounter++;
expect(true)->toBeTrue(); expect(true)->toBeTrue();
}); });
test('second in nested describe', function () use (&$runCounter): void { test('second in nested describe', function () use (&$runCounter) {
expect($runCounter)->toBe(1); expect($runCounter)->toBe(1);
$runCounter++; $runCounter++;
})->depends('first in nested describe'); })->depends('first in nested describe');
test('third in nested describe', function () use (&$runCounter): void { test('third in nested describe', function () use (&$runCounter) {
expect($runCounter)->toBe(2); expect($runCounter)->toBe(2);
})->depends('second in nested describe'); })->depends('second in nested describe');
}); });
}); });
test('depends on test after describe block', function () use (&$runCounter): void { test('depends on test after describe block', function () use (&$runCounter) {
expect($runCounter)->toBe(2); expect($runCounter)->toBe(2);
})->depends('first', 'second'); })->depends('first', 'second');
+5 -5
View File
@@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase;
class InheritanceTest extends TestCase class InheritanceTest extends TestCase
{ {
public function foo(): string public function foo()
{ {
return 'bar'; return 'bar';
} }
@@ -12,11 +12,11 @@ class InheritanceTest extends TestCase
pest()->extend(InheritanceTest::class); pest()->extend(InheritanceTest::class);
it('is a test', function (): void { it('is a test', function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
}); });
it('uses correct parent class', function (): void { it('uses correct parent class', function () {
expect(get_parent_class($this))->toEqual(InheritanceTest::class) expect(get_parent_class($this))->toEqual(InheritanceTest::class);
->and($this->foo())->toEqual('bar'); expect($this->foo())->toEqual('bar');
})->depends('it is a test'); })->depends('it is a test');
+3 -5
View File
@@ -1,14 +1,12 @@
<?php <?php
declare(strict_types=1); test('deprecated', function () {
str_contains(null, null);
test('deprecated', function (): void {
str_contains('', '');
expect(true)->toBeTrue(); expect(true)->toBeTrue();
}); });
test('user deprecated', function (): void { test('user deprecated', function () {
trigger_error('Since foo 1.0: This is a deprecation description', \E_USER_DEPRECATED); trigger_error('Since foo 1.0: This is a deprecation description', \E_USER_DEPRECATED);
expect(true)->toBeTrue(); expect(true)->toBeTrue();
+27 -27
View File
@@ -2,58 +2,58 @@
beforeEach(fn () => $this->count = 1); beforeEach(fn () => $this->count = 1);
test('before each', function (): void { test('before each', function () {
expect($this->count)->toBe(1); expect($this->count)->toBe(1);
}); });
describe('hooks', function (): void { describe('hooks', function () {
beforeEach(function (): void { beforeEach(function () {
$this->count++; $this->count++;
}); });
test('value', function (): void { test('value', function () {
expect($this->count)->toBe(2); expect($this->count)->toBe(2);
$this->count++; $this->count++;
}); });
afterEach(function (): void { afterEach(function () {
expect($this->count)->toBe(3); expect($this->count)->toBe(3);
}); });
}); });
describe('hooks in different orders', function (): void { describe('hooks in different orders', function () {
beforeEach(function (): void { beforeEach(function () {
$this->count++; $this->count++;
}); });
test('value', function (): void { test('value', function () {
expect($this->count)->toBe(3); expect($this->count)->toBe(3);
$this->count++; $this->count++;
}); });
afterEach(function (): void { afterEach(function () {
expect($this->count)->toBe(4); expect($this->count)->toBe(4);
}); });
beforeEach(function (): void { beforeEach(function () {
$this->count++; $this->count++;
}); });
}); });
test('todo')->todo()->shouldNotRun(); test('todo')->todo()->shouldNotRun();
test('previous describable before each does not get applied here', function (): void { test('previous describable before each does not get applied here', function () {
expect($this->count)->toBe(1); expect($this->count)->toBe(1);
}); });
describe('todo on hook', function (): void { describe('todo on hook', function () {
beforeEach()->todo(); beforeEach()->todo();
test('should not fail')->shouldNotRun(); test('should not fail')->shouldNotRun();
test('should run')->expect(true)->toBeTrue(); test('should run')->expect(true)->toBeTrue();
}); });
describe('todo on describe', function (): void { describe('todo on describe', function () {
test('should not fail')->shouldNotRun(); test('should not fail')->shouldNotRun();
test('should run')->expect(true)->toBeTrue(); 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]); test('with', fn ($foo) => expect($foo)->toBe(1))->with([1]);
describe('with on hook', function (): void { describe('with on hook', function () {
beforeEach()->with([2]); beforeEach()->with([2]);
test('value', function ($foo): void { test('value', function ($foo) {
expect($foo)->toBe(2); expect($foo)->toBe(2);
}); });
}); });
describe('with on describe', function (): void { describe('with on describe', function () {
test('value', function ($foo): void { test('value', function ($foo) {
expect($foo)->toBe(3); expect($foo)->toBe(3);
}); });
})->with([3]); })->with([3]);
describe('depends on describe', function (): void { describe('depends on describe', function () {
test('foo', function (): void { test('foo', function () {
expect('foo')->toBe('foo'); expect('foo')->toBe('foo');
}); });
test('bar', function (): void { test('bar', function () {
expect('bar')->toBe('bar'); expect('bar')->toBe('bar');
})->depends('foo'); })->depends('foo');
}); });
describe('depends on describe using with', function (): void { describe('depends on describe using with', function () {
test('foo', function ($foo): void { test('foo', function ($foo) {
expect($foo)->toBe(3); expect($foo)->toBe(3);
}); });
test('bar', function ($foo): void { test('bar', function ($foo) {
expect($foo + $foo)->toBe(6); expect($foo + $foo)->toBe(6);
})->depends('foo'); })->depends('foo');
})->with([3]); })->with([3]);
describe('with test after describe', function (): void { describe('with test after describe', function () {
beforeEach(function (): void { beforeEach(function () {
$this->count++; $this->count++;
}); });
describe('foo', function (): void {}); describe('foo', function () {});
it('should run the before each', function (): void { it('should run the before each', function () {
expect($this->count)->toBe(2); 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')->get('bar')->expect(true)->toBeTrue();
get('foo')->expect(true)->toBeTrue(); get('foo')->expect(true)->toBeTrue();
describe('a "describe" group of tests', function (): void { describe('a "describe" group of tests', function () {
get('foo'); // not incomplete because closure is created... get('foo'); // not incomplete because closure is created...
get('foo')->get('bar')->expect(true)->toBeTrue(); get('foo')->get('bar')->expect(true)->toBeTrue();
get('foo')->expect(true)->toBeTrue(); get('foo')->expect(true)->toBeTrue();
+4 -6
View File
@@ -1,19 +1,17 @@
<?php <?php
declare(strict_types=1); it('may have an associated assignee', function () {
it('may have an associated assignee', function (): void {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->done(assignee: 'nunomaduro'); })->done(assignee: 'nunomaduro');
it('may have an associated issue', function (): void { it('may have an associated issue', function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->done(issue: 1); })->done(issue: 1);
it('may have an associated PR', function (): void { it('may have an associated PR', function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->done(pr: 1); })->done(pr: 1);
it('may have an associated note', function (): void { it('may have an associated note', function () {
expect(true)->toBeTrue(); expect(true)->toBeTrue();
})->done(note: 'a note'); })->done(note: 'a note');
+33 -27
View File
@@ -1,89 +1,95 @@
<?php <?php
it('gives access the the underlying expectException', function (): void { it('gives access the the underlying expectException', function () {
expect(fn () => throw new InvalidArgumentException)->toThrow(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
throw new InvalidArgumentException;
}); });
it('catch exceptions', function (): void { it('catch exceptions', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throws(Exception::class); })->throws(Exception::class);
it('catch exceptions and messages', function (): void { it('catch exceptions and messages', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throws(Exception::class, 'Something bad happened'); })->throws(Exception::class, 'Something bad happened');
it('catch exceptions, messages and code', function (): void { it('catch exceptions, messages and code', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throws(Exception::class, 'Something bad happened', 1); })->throws(Exception::class, 'Something bad happened', 1);
it('can just define the message', function (): void { it('can just define the message', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throws('Something bad happened'); })->throws('Something bad happened');
it('can just define the code', function (): void { it('can just define the code', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throws(1); })->throws(1);
it('not catch exceptions if given condition is false', function (): void { it('not catch exceptions if given condition is false', function () {
expect(true)->toBeTrue(); $this->assertTrue(true);
})->throwsIf(false, Exception::class); })->throwsIf(false, Exception::class);
it('catch exceptions if given condition is true', function (): void { it('catch exceptions if given condition is true', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsIf(fn (): true => true, Exception::class); })->throwsIf(function () {
return true;
}, Exception::class);
it('catch exceptions and messages if given condition is true', function (): void { it('catch exceptions and messages if given condition is true', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsIf(true, Exception::class, 'Something bad happened'); })->throwsIf(true, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is true', function (): void { it('catch exceptions, messages and code if given condition is true', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throwsIf(true, Exception::class, 'Something bad happened', 1); })->throwsIf(true, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is true', function (): void { it('can just define the message if given condition is true', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsIf(true, 'Something bad happened'); })->throwsIf(true, 'Something bad happened');
it('can just define the code if given condition is true', function (): void { it('can just define the code if given condition is true', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throwsIf(true, 1); })->throwsIf(true, 1);
it('can just define the message if given condition is 1', function (): void { it('can just define the message if given condition is 1', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsIf(1, 'Something bad happened'); })->throwsIf(1, 'Something bad happened');
it('can just define the code if given condition is 1', function (): void { it('can just define the code if given condition is 1', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throwsIf(1, 1); })->throwsIf(1, 1);
it('not catch exceptions if given condition is true', function (): void { it('not catch exceptions if given condition is true', function () {
expect(true)->toBeTrue(); $this->assertTrue(true);
})->throwsUnless(true, Exception::class); })->throwsUnless(true, Exception::class);
it('catch exceptions if given condition is false', function (): void { it('catch exceptions if given condition is false', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsUnless(fn (): false => false, Exception::class); })->throwsUnless(function () {
return false;
}, Exception::class);
it('catch exceptions and messages if given condition is false', function (): void { it('catch exceptions and messages if given condition is false', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsUnless(false, Exception::class, 'Something bad happened'); })->throwsUnless(false, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is false', function (): void { it('catch exceptions, messages and code if given condition is false', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throwsUnless(false, Exception::class, 'Something bad happened', 1); })->throwsUnless(false, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is false', function (): void { it('can just define the message if given condition is false', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsUnless(false, 'Something bad happened'); })->throwsUnless(false, 'Something bad happened');
it('can just define the code if given condition is false', function (): void { it('can just define the code if given condition is false', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throwsUnless(false, 1); })->throwsUnless(false, 1);
it('can just define the message if given condition is 0', function (): void { it('can just define the message if given condition is 0', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsUnless(0, 'Something bad happened'); })->throwsUnless(0, 'Something bad happened');
it('can just define the code if given condition is 0', function (): void { it('can just define the code if given condition is 0', function () {
throw new Exception('Something bad happened', 1); throw new Exception('Something bad happened', 1);
})->throwsUnless(0, 1); })->throwsUnless(0, 1);
+24 -23
View File
@@ -1,72 +1,73 @@
<?php <?php
it('can access methods', function (): void { it('can access methods', function () {
expect(new HasMethods) expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods'); ->name()->toBeString()->toEqual('Has Methods');
}); });
it('can access multiple methods', function (): void { it('can access multiple methods', function () {
expect(new HasMethods) expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods') ->name()->toBeString()->toEqual('Has Methods')
->quantity()->toBeInt()->toEqual(20); ->quantity()->toBeInt()->toEqual(20);
}); });
it('works with not', function (): void { it('works with not', function () {
expect(new HasMethods) expect(new HasMethods)
->name()->not->toEqual('world')->toEqual('Has Methods') ->name()->not->toEqual('world')->toEqual('Has Methods')
->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull; ->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull;
}); });
it('can accept arguments', function (): void { it('can accept arguments', function () {
expect(new HasMethods) expect(new HasMethods)
->multiply(5, 4)->toBeInt->toEqual(20); ->multiply(5, 4)->toBeInt->toEqual(20);
}); });
it('works with each', function (): void { it('works with each', function () {
expect(new HasMethods) expect(new HasMethods)
->attributes()->toBeArray->each->not()->toBeNull ->attributes()->toBeArray->each->not()->toBeNull
->attributes()->each(function ($attribute): void { ->attributes()->each(function ($attribute) {
$attribute->not->toBeNull(); $attribute->not->toBeNull();
}); });
}); });
it('works inside of each', function (): void { it('works inside of each', function () {
expect(new HasMethods) expect(new HasMethods)
->books()->each(function ($book): void { ->books()->each(function ($book) {
$book->title->not->toBeNull->cost->toBeGreaterThan(19); $book->title->not->toBeNull->cost->toBeGreaterThan(19);
}); });
}); });
it('works with sequence', function (): void { it('works with sequence', function () {
expect(new HasMethods) expect(new HasMethods)
->books()->sequence( ->books()->sequence(
function ($book): void { function ($book) {
$book->title->toEqual('Foo')->cost->toEqual(20); $book->title->toEqual('Foo')->cost->toEqual(20);
}, },
function ($book): void { function ($book) {
$book->title->toEqual('Bar')->cost->toEqual(30); $book->title->toEqual('Bar')->cost->toEqual(30);
}, },
); );
}); });
it('can compose complex expectations', function (): void { it('can compose complex expectations', function () {
expect(new HasMethods) expect(new HasMethods)
->toBeObject()
->name()->toEqual('Has Methods')->not()->toEqual('bar') ->name()->toEqual('Has Methods')->not()->toEqual('bar')
->quantity()->not->toEqual('world')->toEqual(20)->toBeInt ->quantity()->not->toEqual('world')->toEqual(20)->toBeInt
->multiply(3, 4)->not->toBeString->toEqual(12) ->multiply(3, 4)->not->toBeString->toEqual(12)
->attributes()->toBeArray() ->attributes()->toBeArray()
->books()->toBeArray->each->not->toBeEmpty ->books()->toBeArray->each->not->toBeEmpty
->books()->sequence( ->books()->sequence(
function ($book): void { function ($book) {
$book->title->toEqual('Foo')->cost->toEqual(20); $book->title->toEqual('Foo')->cost->toEqual(20);
}, },
function ($book): void { function ($book) {
$book->title->toEqual('Bar')->cost->toEqual(30); $book->title->toEqual('Bar')->cost->toEqual(30);
}, },
); );
}); });
it('can handle nested method calls', function (): void { it('can handle nested method calls', function () {
expect(new HasMethods) expect(new HasMethods)
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString() ->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
->newInstance()->name()->toEqual('Has Methods')->not->toBeInt ->newInstance()->name()->toEqual('Has Methods')->not->toBeInt
@@ -81,7 +82,7 @@ it('works with higher order tests')
->name()->toEqual('Has Methods') ->name()->toEqual('Has Methods')
->books()->each->toBeArray; ->books()->each->toBeArray;
it('can use the scoped method to lock into the given level for expectations', function (): void { it('can use the scoped method to lock into the given level for expectations', function () {
expect(new HasMethods) expect(new HasMethods)
->attributes()->scoped(fn ($attributes) => $attributes ->attributes()->scoped(fn ($attributes) => $attributes
->name->toBe('Has Methods') ->name->toBe('Has Methods')
@@ -98,7 +99,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 (): void { it('works consistently with the json expectation method', function () {
expect(new HasMethods) expect(new HasMethods)
->jsonString()->json()->id->toBe(1) ->jsonString()->json()->id->toBe(1)
->jsonString()->json()->name->toBe('Has Methods')->toBeString() ->jsonString()->json()->name->toBe('Has Methods')->toBeString()
@@ -112,22 +113,22 @@ class HasMethods
return '{ "id": 1, "name": "Has Methods", "quantity": 20 }'; return '{ "id": 1, "name": "Has Methods", "quantity": 20 }';
} }
public function name(): string public function name()
{ {
return 'Has Methods'; return 'Has Methods';
} }
public function quantity(): int public function quantity()
{ {
return 20; return 20;
} }
public function multiply($x, $y): int|float public function multiply($x, $y)
{ {
return $x * $y; return $x * $y;
} }
public function attributes(): array public function attributes()
{ {
return [ return [
'name' => $this->name(), 'name' => $this->name(),
@@ -135,7 +136,7 @@ class HasMethods
]; ];
} }
public function books(): array public function books()
{ {
return [ return [
[ [
@@ -149,7 +150,7 @@ class HasMethods
]; ];
} }
public function newInstance(): static public function newInstance()
{ {
return new static; return new static;
} }
@@ -1,24 +1,24 @@
<?php <?php
it('can access methods and properties', function (): void { it('can access methods and properties', function () {
expect(new HasMethodsAndProperties) expect(new HasMethodsAndProperties)
->name->toEqual('Has Methods and Properties')->not()->toEqual('bar') ->name->toEqual('Has Methods and Properties')->not()->toEqual('bar')
->multiply(3, 4)->not->toBeString->toEqual(12) ->multiply(3, 4)->not->toBeString->toEqual(12)
->posts->each(function ($post): void { ->posts->each(function ($post) {
$post->is_published->toBeTrue; $post->is_published->toBeTrue;
})->books()->toBeArray() })->books()->toBeArray()
->posts->toBeArray->each->not->toBeEmpty ->posts->toBeArray->each->not->toBeEmpty
->books()->sequence( ->books()->sequence(
function ($book): void { function ($book) {
$book->title->toEqual('Foo')->cost->toEqual(20); $book->title->toEqual('Foo')->cost->toEqual(20);
}, },
function ($book): void { function ($book) {
$book->title->toEqual('Bar')->cost->toEqual(30); $book->title->toEqual('Bar')->cost->toEqual(30);
}, },
); );
}); });
it('can handle nested methods and properties', function (): void { it('can handle nested methods and properties', function () {
expect(new HasMethodsAndProperties) expect(new HasMethodsAndProperties)
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt ->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
->newInstance()->meta->foo->toBeArray() ->newInstance()->meta->foo->toBeArray()
@@ -33,14 +33,15 @@ it('works with higher order tests')
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5) ->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
->newInstance()->books()->toBeArray(); ->newInstance()->books()->toBeArray();
it('can start a new higher order expectation using the and syntax', function (): void { it('can start a new higher order expectation using the and syntax', function () {
expect(new HasMethodsAndProperties) expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class) ->toBeInstanceOf(HasMethodsAndProperties::class)
->meta->toBeArray ->meta->toBeArray
->and(['foo' => 'bar']) ->and(['foo' => 'bar'])
->toBeArray() ->toBeArray()
->foo->toEqual('bar') ->foo->toEqual('bar');
->and(static::getCount())->toEqual(4);
expect(static::getCount())->toEqual(4);
}); });
it('can start a new higher order expectation using the and syntax in higher order tests') it('can start a new higher order expectation using the and syntax in higher order tests')
@@ -51,12 +52,12 @@ it('can start a new higher order expectation using the and syntax in higher orde
->toBeArray() ->toBeArray()
->foo->toEqual('bar'); ->foo->toEqual('bar');
it('can start a new higher order expectation using the and syntax without nesting expectations', function (): void { it('can start a new higher order expectation using the and syntax without nesting expectations', function () {
expect(new HasMethodsAndProperties) expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class) ->toBeInstanceOf(HasMethodsAndProperties::class)
->meta ->meta
->sequence( ->sequence(
function ($value, $key): void { function ($value, $key) {
$value->toBeArray()->and($key)->toBe('foo'); $value->toBeArray()->and($key)->toBe('foo');
}, },
); );
@@ -79,7 +80,7 @@ class HasMethodsAndProperties
], ],
]; ];
public function books(): array public function books()
{ {
return [ return [
[ [
@@ -93,12 +94,12 @@ class HasMethodsAndProperties
]; ];
} }
public function multiply($x, $y): int|float public function multiply($x, $y)
{ {
return $x * $y; return $x * $y;
} }
public function newInstance(): static public function newInstance()
{ {
return new static; return new static;
} }
@@ -1,50 +1,50 @@
<?php <?php
it('allows properties to be accessed from the value', function (): void { it('allows properties to be accessed from the value', function () {
expect(['foo' => 1])->foo->toBeInt()->toEqual(1); expect(['foo' => 1])->foo->toBeInt()->toEqual(1);
}); });
it('can access multiple properties from the value', function (): void { it('can access multiple properties from the value', function () {
expect(['foo' => 'bar', 'hello' => 'world']) expect(['foo' => 'bar', 'hello' => 'world'])
->foo->toBeString()->toEqual('bar') ->foo->toBeString()->toEqual('bar')
->hello->toBeString()->toEqual('world'); ->hello->toBeString()->toEqual('world');
}); });
it('works with not', function (): void { it('works with not', function () {
expect(['foo' => 'bar', 'hello' => 'world']) expect(['foo' => 'bar', 'hello' => 'world'])
->foo->not->not->toEqual('bar') ->foo->not->not->toEqual('bar')
->foo->not->toEqual('world')->toEqual('bar') ->foo->not->toEqual('world')->toEqual('bar')
->hello->toEqual('world')->not()->toEqual('bar')->not->toBeNull; ->hello->toEqual('world')->not()->toEqual('bar')->not->toBeNull;
}); });
it('works with each', function (): void { it('works with each', function () {
expect(['numbers' => [1, 2, 3, 4], 'words' => ['hey', 'there']]) expect(['numbers' => [1, 2, 3, 4], 'words' => ['hey', 'there']])
->numbers->toEqual([1, 2, 3, 4])->each->toBeInt->toBeLessThan(5) ->numbers->toEqual([1, 2, 3, 4])->each->toBeInt->toBeLessThan(5)
->words->each(function ($word): void { ->words->each(function ($word) {
$word->toBeString()->not->toBeInt(); $word->toBeString()->not->toBeInt();
}); });
}); });
it('works inside of each', function (): void { it('works inside of each', function () {
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]]) expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
->books->each(function ($book): void { ->books->each(function ($book) {
$book->title->not->toBeNull->cost->toBeGreaterThan(19); $book->title->not->toBeNull->cost->toBeGreaterThan(19);
}); });
}); });
it('works with sequence', function (): void { it('works with sequence', function () {
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]]) expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
->books->sequence( ->books->sequence(
function ($book): void { function ($book) {
$book->title->toEqual('Foo')->cost->toEqual(20); $book->title->toEqual('Foo')->cost->toEqual(20);
}, },
function ($book): void { function ($book) {
$book->title->toEqual('Bar')->cost->toEqual(30); $book->title->toEqual('Bar')->cost->toEqual(30);
}, },
); );
}); });
it('can compose complex expectations', function (): void { it('can compose complex expectations', function () {
expect(['foo' => 'bar', 'numbers' => [1, 2, 3, 4]]) expect(['foo' => 'bar', 'numbers' => [1, 2, 3, 4]])
->toContain('bar')->toBeArray() ->toContain('bar')->toBeArray()
->numbers->toEqual([1, 2, 3, 4])->not()->toEqual('bar')->each->toBeInt ->numbers->toEqual([1, 2, 3, 4])->not()->toEqual('bar')->each->toBeInt
@@ -52,23 +52,23 @@ it('can compose complex expectations', function (): void {
->numbers->toBeArray(); ->numbers->toBeArray();
}); });
it('works with objects', function (): void { it('works with objects', function () {
expect(new HasProperties) expect(new HasProperties)
->name->toEqual('foo')->not->toEqual('world') ->name->toEqual('foo')->not->toEqual('world')
->posts->toHaveCount(2)->each(function ($post): void { ->posts->toHaveCount(2)->each(function ($post) {
$post->is_published->toBeTrue(); $post->is_published->toBeTrue();
}) })
->posts->sequence( ->posts->sequence(
function ($post): void { function ($post) {
$post->title->toEqual('Foo'); $post->title->toEqual('Foo');
}, },
function ($post): void { function ($post) {
$post->title->toEqual('Bar'); $post->title->toEqual('Bar');
}, },
); );
}); });
it('works with nested properties', function (): void { it('works with nested properties', function () {
expect(new HasProperties) expect(new HasProperties)
->nested->foo->bar->toBeString()->toEqual('baz') ->nested->foo->bar->toBeString()->toEqual('baz')
->posts->toBeArray()->toHaveCount(2); ->posts->toBeArray()->toHaveCount(2);
+30 -24
View File
@@ -2,15 +2,16 @@
use Pest\Expectation; use Pest\Expectation;
test('an exception is thrown if the the type is not iterable', function (): void { test('an exception is thrown if the the type is not iterable', function () {
expect('Foobar')->each()->toEqual('Foobar'); expect('Foobar')->each()->toEqual('Foobar');
})->throws(BadMethodCallException::class, 'Expectation value is not iterable.'); })->throws(BadMethodCallException::class, 'Expectation value is not iterable.');
it('expects on each item', function (): void { it('expects on each item', function () {
expect([1, 1, 1]) expect([1, 1, 1])
->each() ->each()
->toEqual(1) ->toEqual(1);
->and(static::getCount())->toBe(3); // + 1 assertion
expect(static::getCount())->toBe(3); // + 1 assertion
expect([1, 1, 1]) expect([1, 1, 1])
->each ->each
@@ -19,12 +20,13 @@ it('expects on each item', function (): void {
expect(static::getCount())->toBe(7); expect(static::getCount())->toBe(7);
}); });
it('chains expectations on each item', function (): void { it('chains expectations on each item', function () {
expect([1, 1, 1]) expect([1, 1, 1])
->each() ->each()
->toBeInt() ->toBeInt()
->toEqual(1) ->toEqual(1);
->and(static::getCount())->toBe(6); // + 1 assertion
expect(static::getCount())->toBe(6); // + 1 assertion
expect([2, 2, 2]) expect([2, 2, 2])
->each ->each
@@ -34,12 +36,13 @@ it('chains expectations on each item', function (): void {
expect(static::getCount())->toBe(13); expect(static::getCount())->toBe(13);
}); });
test('opposite expectations on each item', function (): void { test('opposite expectations on each item', function () {
expect([1, 2, 3]) expect([1, 2, 3])
->each() ->each()
->not() ->not()
->toEqual(4) ->toEqual(4);
->and(static::getCount())->toBe(3);
expect(static::getCount())->toBe(3);
expect([1, 2, 3]) expect([1, 2, 3])
->each() ->each()
@@ -48,16 +51,17 @@ test('opposite expectations on each item', function (): void {
expect(static::getCount())->toBe(7); expect(static::getCount())->toBe(7);
}); });
test('chained opposite and non-opposite expectations', function (): void { test('chained opposite and non-opposite expectations', function () {
expect([1, 2, 3]) expect([1, 2, 3])
->each() ->each()
->not() ->not()
->toEqual(4) ->toEqual(4)
->toBeInt() ->toBeInt();
->and(static::getCount())->toBe(6);
expect(static::getCount())->toBe(6);
}); });
it('can add expectations via "and"', function (): void { it('can add expectations via "and"', function () {
expect([1, 2, 3]) expect([1, 2, 3])
->each() ->each()
->toBeInt // + 3 ->toBeInt // + 3
@@ -74,18 +78,20 @@ it('can add expectations via "and"', function (): void {
expect(static::getCount())->toBe(14); expect(static::getCount())->toBe(14);
}); });
it('accepts callables', function (): void { it('accepts callables', function () {
expect([1, 2, 3])->each(function ($number): void { expect([1, 2, 3])->each(function ($number) {
expect($number)->toBeInstanceOf(Expectation::class) expect($number)->toBeInstanceOf(Expectation::class);
->and($number->value)->toBeInt(); expect($number->value)->toBeInt();
$number->toBeInt->not->toBeString; $number->toBeInt->not->toBeString;
}) });
->and(static::getCount())->toBe(12);
expect(static::getCount())->toBe(12);
}); });
it('passes the key of the current item to callables', function (): void { it('passes the key of the current item to callables', function () {
expect([1, 2, 3])->each(function ($number, $key): void { expect([1, 2, 3])->each(function ($number, $key) {
expect($key)->toBeInt(); expect($key)->toBeInt();
}) });
->and(static::getCount())->toBe(3);
expect(static::getCount())->toBe(3);
}); });
+6 -6
View File
@@ -1,29 +1,29 @@
<?php <?php
expect()->extend('toBeAMacroExpectation', function (): object { expect()->extend('toBeAMacroExpectation', function () {
$this->toBeTrue(); $this->toBeTrue();
return $this; return $this;
}); });
expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value): object { expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value) {
$this->toBe($value); $this->toBe($value);
return $this; return $this;
}); });
it('macros true is true', function (): void { it('macros true is true', function () {
expect(true)->toBeAMacroExpectation(); expect(true)->toBeAMacroExpectation();
}); });
it('macros false is not true', function (): void { it('macros false is not true', function () {
expect(false)->not->toBeAMacroExpectation(); expect(false)->not->toBeAMacroExpectation();
}); });
it('macros true is true with argument', function (): void { it('macros true is true with argument', function () {
expect(true)->toBeAMacroExpectationWithArguments(true); expect(true)->toBeAMacroExpectationWithArguments(true);
}); });
it('macros false is not true with argument', function (): void { it('macros false is not true with argument', function () {
expect(false)->not->toBeAMacroExpectationWithArguments(true); expect(false)->not->toBeAMacroExpectationWithArguments(true);
}); });
+2 -2
View File
@@ -2,13 +2,13 @@
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
test('it properly parses json string', function (): void { test('it properly parses json string', function () {
expect('{"name":"Nuno"}') expect('{"name":"Nuno"}')
->json() ->json()
->name ->name
->toBe('Nuno'); ->toBe('Nuno');
}); });
test('fails with broken json string', function (): void { test('fails with broken json string', function () {
expect('{":"Nuno"}')->json(); expect('{":"Nuno"}')->json();
})->throws(ExpectationFailedException::class); })->throws(ExpectationFailedException::class);
+45 -26
View File
@@ -2,11 +2,11 @@
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
beforeEach(function (): void { beforeEach(function () {
$this->matched = null; $this->matched = null;
}); });
it('pass', function (): void { it('pass', function () {
expect('baz') expect('baz')
->match('foo', [ ->match('foo', [
'bar' => function ($value) { 'bar' => function ($value) {
@@ -21,20 +21,25 @@ it('pass', function (): void {
}, },
] ]
) )
->toEqual($this->matched) ->toEqual($this->matched);
->and(static::getCount())->toBe(2);
expect(static::getCount())->toBe(2);
}); });
it('failures', function (): void { it('failures', function () {
expect(true) expect(true)
->match('foo', [ ->match('foo', [
'bar' => fn ($value) => $value->toBeTrue(), 'bar' => function ($value) {
'foo' => fn ($value) => $value->toBeFalse(), return $value->toBeTrue();
},
'foo' => function ($value) {
return $value->toBeFalse();
},
] ]
); );
})->throws(ExpectationFailedException::class, 'true is false'); })->throws(ExpectationFailedException::class, 'true is false');
it('runs with truthy', function (): void { it('runs with truthy', function () {
expect('foo') expect('foo')
->match(1, [ ->match(1, [
'bar' => function ($value) { 'bar' => function ($value) {
@@ -49,11 +54,12 @@ it('runs with truthy', function (): void {
}, },
] ]
) )
->toEqual($this->matched) ->toEqual($this->matched);
->and(static::getCount())->toBe(2);
expect(static::getCount())->toBe(2);
}); });
it('runs with falsy', function (): void { it('runs with falsy', function () {
expect('foo') expect('foo')
->match(false, [ ->match(false, [
'bar' => function ($value) { 'bar' => function ($value) {
@@ -68,14 +74,17 @@ it('runs with falsy', function (): void {
}, },
] ]
) )
->toEqual($this->matched) ->toEqual($this->matched);
->and(static::getCount())->toBe(2);
expect(static::getCount())->toBe(2);
}); });
it('runs with truthy closure condition', function (): void { it('runs with truthy closure condition', function () {
expect('foo') expect('foo')
->match( ->match(
fn (): string => '1', [ function () {
return '1';
}, [
'bar' => function ($value) { 'bar' => function ($value) {
$this->matched = 'bar'; $this->matched = 'bar';
@@ -88,14 +97,17 @@ it('runs with truthy closure condition', function (): void {
}, },
] ]
) )
->toEqual($this->matched) ->toEqual($this->matched);
->and(static::getCount())->toBe(2);
expect(static::getCount())->toBe(2);
}); });
it('runs with falsy closure condition', function (): void { it('runs with falsy closure condition', function () {
expect('foo') expect('foo')
->match( ->match(
fn (): string => '0', [ function () {
return '0';
}, [
'bar' => function ($value) { 'bar' => function ($value) {
$this->matched = 'bar'; $this->matched = 'bar';
@@ -108,11 +120,12 @@ it('runs with falsy closure condition', function (): void {
}, },
] ]
) )
->toEqual($this->matched) ->toEqual($this->matched);
->and(static::getCount())->toBe(2);
expect(static::getCount())->toBe(2);
}); });
it('can be passed non-callable values', function (): void { it('can be passed non-callable values', function () {
expect('foo') expect('foo')
->match('pest', [ ->match('pest', [
'bar' => 'foo', 'bar' => 'foo',
@@ -121,15 +134,21 @@ it('can be passed non-callable values', function (): void {
); );
})->throws(ExpectationFailedException::class, 'two strings are equal'); })->throws(ExpectationFailedException::class, 'two strings are equal');
it('fails with unhandled match', function (): void { it('fails with unhandled match', function () {
expect('foo')->match('bar', []); expect('foo')->match('bar', []);
})->throws(ExpectationFailedException::class, 'Unhandled match value.'); })->throws(ExpectationFailedException::class, 'Unhandled match value.');
it('can be used in higher order tests') it('can be used in higher order tests')
->expect(true) ->expect(true)
->match( ->match(
fn (): true => true, [ function () {
false => fn ($value) => $value->toBeFalse(), return true;
true => fn ($value) => $value->toBeTrue(), }, [
false => function ($value) {
return $value->toBeFalse();
},
true => function ($value) {
return $value->toBeTrue();
},
] ]
); );

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