mirror of
https://github.com/pestphp/pest.git
synced 2026-07-21 17:10:03 +02:00
Compare commits
70 Commits
v4.7.3
...
b52bbb4cca
| Author | SHA1 | Date | |
|---|---|---|---|
| b52bbb4cca | |||
| 52819501eb | |||
| 7b18287374 | |||
| 33e3bf2888 | |||
| 72ab506d7b | |||
| 5dc49a71d6 | |||
| 1adb484d3f | |||
| aadf7e92a6 | |||
| df42b9ed7b | |||
| 632ae79c8b | |||
| b5fed82883 | |||
| 145e20db75 | |||
| ee2e97e932 | |||
| b561236325 | |||
| e685179b7e | |||
| ac8feafdcc | |||
| b49cd150c9 | |||
| 5cfb4133bf | |||
| 3c8bae5f05 | |||
| b2998bc69e | |||
| 3876093cd2 | |||
| 932f8bcc07 | |||
| d393799d2a | |||
| 0d7814ca16 | |||
| 8467c64c22 | |||
| 97714a7088 | |||
| afb582616d | |||
| 15e9b6a507 | |||
| 520ce29376 | |||
| 774a340400 | |||
| 3d5bba93f8 | |||
| 79bc7a8257 | |||
| fc48c1bd1e | |||
| da726beffc | |||
| 4ef12b9aac | |||
| 4d550cecfd | |||
| 34695843b3 | |||
| d17be9decd | |||
| b828ddcec7 | |||
| f859bb179d | |||
| 18bbca748f | |||
| f142aad8ad | |||
| 74a28d4f5e | |||
| 6053e15d00 | |||
| 2d649d765f | |||
| 4fb4908570 | |||
| e63a886f98 | |||
| 8dd650fd05 | |||
| fbca346d7c | |||
| 3f13bca0f7 | |||
| d3acb1c56a | |||
| e601e6df31 | |||
| 6fdbca1226 | |||
| 54359b895f | |||
| 44c04bfce1 | |||
| 271c680d3c | |||
| 4a1d8d27b8 | |||
| 0f6924984c | |||
| 668ca9f5de | |||
| f659a45311 | |||
| 12c1da29ee | |||
| fa27c8daef | |||
| f0a08f0503 | |||
| 2c040c5b1f | |||
| a9ce1fd739 | |||
| 3533356262 | |||
| 4aa41d0b14 | |||
| e4ed60085c | |||
| e2b119655d | |||
| fcf5baf0a9 |
@@ -2,7 +2,7 @@ name: Static Analysis
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [4.x]
|
branches: [5.x]
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 9 * * *'
|
- cron: '0 9 * * *'
|
||||||
@@ -28,12 +28,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||||
with:
|
with:
|
||||||
php-version: 8.3
|
php-version: 8.4
|
||||||
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
key: static-php-8.4-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
static-php-8.3-${{ matrix.dependency-version }}-composer-
|
static-php-8.4-${{ matrix.dependency-version }}-composer-
|
||||||
static-php-8.3-composer-
|
static-php-8.4-composer-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_ROOT_VERSION: 4.x-dev
|
COMPOSER_ROOT_VERSION: 5.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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Tests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [4.x]
|
branches: [5.x]
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 9 * * *'
|
- cron: '0 9 * * *'
|
||||||
@@ -24,18 +24,15 @@ jobs:
|
|||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest] # windows-latest
|
os: [ubuntu-latest, macos-latest] # windows-latest
|
||||||
symfony: ['7.4', '8.0']
|
symfony: ['8.0']
|
||||||
php: ['8.3', '8.4', '8.5']
|
php: ['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 }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||||
@@ -51,7 +48,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # 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') }}
|
||||||
@@ -67,7 +64,7 @@ jobs:
|
|||||||
- name: Install PHP dependencies
|
- name: Install PHP dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
COMPOSER_ROOT_VERSION: 4.x-dev
|
COMPOSER_ROOT_VERSION: 5.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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/pestphp/art/master/v4/social.png" width="600" alt="PEST">
|
<img src="https://raw.githubusercontent.com/pestphp/art/master/v5/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 (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://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://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 v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**.
|
> Pest v5 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v5-is-here)**.
|
||||||
|
|
||||||
**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
@@ -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 v3 you should use the `3.x` branch instead.**
|
> **For Pest v4 you should use the `4.x` branch instead.**
|
||||||
|
|
||||||
- Clear your local repository with: `git add . && git reset --hard && git checkout 4.x`
|
- Clear your local repository with: `git add . && git reset --hard && git checkout 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)
|
- 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)
|
||||||
- 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"`
|
||||||
|
|||||||
+51
-15
@@ -29,22 +29,54 @@ async function loadRolldown() {
|
|||||||
return await import(pathToFileURL(path).href)
|
return await import(pathToFileURL(path).href)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readJsonWithComments(path) {
|
export function stripJsonComments(raw) {
|
||||||
const raw = await readFile(path, 'utf8')
|
let out = ''
|
||||||
const stripped = raw
|
let inString = false
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
let quote = ''
|
||||||
.replace(/(^|[^:])\/\/[^\n]*/g, '$1')
|
let inLine = false
|
||||||
return JSON.parse(stripped)
|
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 loadAliasFromTsconfig() {
|
async function readJsonWithComments(path) {
|
||||||
|
const raw = await readFile(path, 'utf8')
|
||||||
|
return JSON.parse(stripJsonComments(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAliasFromTsconfig(projectRoot = PROJECT_ROOT) {
|
||||||
const alias = {}
|
const alias = {}
|
||||||
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
||||||
const p = join(PROJECT_ROOT, name)
|
const p = join(projectRoot, 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(PROJECT_ROOT, cfg?.compilerOptions?.baseUrl ?? '.')
|
const baseUrl = resolve(projectRoot, 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
|
||||||
@@ -230,10 +262,14 @@ async function main() {
|
|||||||
process.stdout.write(JSON.stringify(payload))
|
process.stdout.write(JSON.stringify(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const invokedDirectly = process.argv[1] !== undefined
|
||||||
void pathToFileURL
|
&& import.meta.url === pathToFileURL(process.argv[1]).href
|
||||||
await main()
|
|
||||||
} catch (err) {
|
if (invokedDirectly) {
|
||||||
process.stderr.write(String(err?.stack ?? err ?? 'unknown error'))
|
try {
|
||||||
process.exit(1)
|
await main()
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(String(err?.stack ?? err ?? 'unknown error'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-15
@@ -17,21 +17,20 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3.0",
|
"php": "^8.4",
|
||||||
"brianium/paratest": "^7.20.0",
|
"brianium/paratest": "^7.23.0",
|
||||||
"composer/xdebug-handler": "^3.0.5",
|
|
||||||
"nunomaduro/collision": "^8.9.4",
|
"nunomaduro/collision": "^8.9.4",
|
||||||
"nunomaduro/termwind": "^2.4.0",
|
"nunomaduro/termwind": "^2.4.0",
|
||||||
"pestphp/pest-plugin": "^4.0.0",
|
"pestphp/pest-plugin": "^5.0.0",
|
||||||
"pestphp/pest-plugin-arch": "^4.0.2",
|
"pestphp/pest-plugin-arch": "^5.0.0",
|
||||||
"pestphp/pest-plugin-mutate": "^4.0.1",
|
"pestphp/pest-plugin-mutate": "^5.0.0",
|
||||||
"pestphp/pest-plugin-profanity": "^4.2.1",
|
"pestphp/pest-plugin-profanity": "^5.0.0",
|
||||||
"phpunit/phpunit": "^12.5.29",
|
"phpunit/phpunit": "^13.2.3",
|
||||||
"symfony/process": "^7.4.13|^8.1.0"
|
"symfony/process": "^8.1.0"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"filp/whoops": "<2.18.3",
|
"filp/whoops": "<2.18.3",
|
||||||
"phpunit/phpunit": ">12.5.29",
|
"phpunit/phpunit": ">13.2.3",
|
||||||
"sebastian/exporter": "<7.0.0",
|
"sebastian/exporter": "<7.0.0",
|
||||||
"webmozart/assert": "<1.11.0"
|
"webmozart/assert": "<1.11.0"
|
||||||
},
|
},
|
||||||
@@ -59,11 +58,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mrpunyapal/peststan": "^0.2.10",
|
"mrpunyapal/peststan": "^0.2.11",
|
||||||
"pestphp/pest-dev-tools": "^4.1.0",
|
"laravel/pao": "^1.1.2",
|
||||||
"pestphp/pest-plugin-browser": "^4.3.1",
|
"pestphp/pest-dev-tools": "^5.0.0",
|
||||||
"pestphp/pest-plugin-type-coverage": "^4.0.4",
|
"pestphp/pest-plugin-browser": "^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,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
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\RemoveParentDelegatingConstructorRector;
|
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
|
||||||
|
use Rector\DeadCode\Rector\ClassMethod\RemoveUselessUnionReturnDocblockRector;
|
||||||
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
|
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
|
||||||
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
|
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
|
||||||
|
|
||||||
@@ -18,6 +20,8 @@ return RectorConfig::configure()
|
|||||||
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
|
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
|
||||||
NarrowObjectReturnTypeRector::class,
|
NarrowObjectReturnTypeRector::class,
|
||||||
RemoveParentDelegatingConstructorRector::class,
|
RemoveParentDelegatingConstructorRector::class,
|
||||||
|
RemoveDuplicatedReturnSelfDocblockRector::class,
|
||||||
|
RemoveUselessUnionReturnDocblockRector::class,
|
||||||
])
|
])
|
||||||
->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:^4.0 --dev</span>
|
<span>composer require pestphp/pest-plugin-browser:^5.0 --dev</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ 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;
|
||||||
@@ -431,15 +433,15 @@ trait Testable
|
|||||||
unset($this->{$property});
|
unset($this->{$property});
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasOutputExpectation = Closure::bind(fn (): bool => is_string($this->outputExpectedString) || is_string($this->outputExpectedRegex), $this, TestCase::class)();
|
$outputBuffer = Closure::bind(fn () => $this->outputBuffer, $this, TestCase::class)();
|
||||||
|
|
||||||
if ($hasOutputExpectation) {
|
if ($outputBuffer->hasExpectation()) {
|
||||||
ob_clean();
|
ob_clean();
|
||||||
|
|
||||||
Closure::bind(function (): void {
|
Closure::bind(function (): void {
|
||||||
$this->outputExpectedString = null;
|
$this->expectedString = null;
|
||||||
$this->outputExpectedRegex = null;
|
$this->expectedRegularExpression = null;
|
||||||
}, $this, TestCase::class)();
|
}, $outputBuffer, OutputBuffer::class)();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setUp();
|
$this->setUp();
|
||||||
@@ -455,7 +457,9 @@ trait Testable
|
|||||||
*/
|
*/
|
||||||
private function __isExpectedException(Throwable $e): bool
|
private function __isExpectedException(Throwable $e): bool
|
||||||
{
|
{
|
||||||
$read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $this, TestCase::class)();
|
$expectation = Closure::bind(fn () => $this->exceptionExpectation, $this, TestCase::class)();
|
||||||
|
|
||||||
|
$read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $expectation, ExceptionExpectation::class)();
|
||||||
|
|
||||||
$expectedClass = $read('expectedException');
|
$expectedClass = $read('expectedException');
|
||||||
|
|
||||||
@@ -463,13 +467,19 @@ trait Testable
|
|||||||
return $e instanceof $expectedClass;
|
return $e instanceof $expectedClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
$expectedMessage = $read('expectedExceptionMessage');
|
$expectedMessage = $read('expectedMessage');
|
||||||
|
|
||||||
if ($expectedMessage !== null) {
|
if ($expectedMessage !== null) {
|
||||||
return str_contains($e->getMessage(), (string) $expectedMessage);
|
return str_contains($e->getMessage(), (string) $expectedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
$expectedCode = $read('expectedExceptionCode');
|
$expectedMessageRegex = $read('expectedMessageRegularExpression');
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
+11
-3
@@ -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,6 +119,14 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-13
@@ -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 = (string) ob_get_clean();
|
$output = 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,15 +921,7 @@ final class Expectation
|
|||||||
|
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$this,
|
$this,
|
||||||
function (ObjectDescription $object) use ($interfaces): bool {
|
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)),
|
||||||
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')),
|
||||||
);
|
);
|
||||||
@@ -1144,8 +1136,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')),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>|self<TOriginalValue, TValue>
|
* @return self<TOriginalValue, mixed>
|
||||||
*/
|
*/
|
||||||
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>|self<TOriginalValue, TValue>
|
* @return self<TOriginalValue, mixed>
|
||||||
*/
|
*/
|
||||||
public function __get(string $name): self
|
public function __get(string $name): self
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -576,15 +576,7 @@ final readonly class OppositeExpectation
|
|||||||
|
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$original,
|
$original,
|
||||||
function (ObjectDescription $object) use ($traits): bool {
|
fn (ObjectDescription $object): bool => array_all($traits, fn (string $trait): bool => ! isset($object->reflectionClass) || ! in_array($trait, $object->reflectionClass->getTraitNames(), true)),
|
||||||
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')),
|
||||||
);
|
);
|
||||||
@@ -604,15 +596,7 @@ final readonly class OppositeExpectation
|
|||||||
|
|
||||||
return Targeted::make(
|
return Targeted::make(
|
||||||
$original,
|
$original,
|
||||||
function (ObjectDescription $object) use ($interfaces): bool {
|
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => ! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)),
|
||||||
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')),
|
||||||
);
|
);
|
||||||
@@ -814,13 +798,11 @@ 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.',
|
||||||
$toString($this->original->value),
|
$exporter->shortenedExport($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 => $toString($argument), $arguments)),
|
implode(' ', array_map(fn (mixed $argument): string => $exporter->export($argument), $arguments)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,8 +834,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')),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ final class TestCaseFactory
|
|||||||
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename);
|
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
|
$realpath = (string) realpath($filename);
|
||||||
|
$filename = str_replace('\\\\', '\\', addslashes($realpath));
|
||||||
$rootPath = TestSuite::getInstance()->rootPath;
|
$rootPath = TestSuite::getInstance()->rootPath;
|
||||||
$relativePath = str_replace($rootPath.DIRECTORY_SEPARATOR, '', $filename);
|
$relativePath = str_replace($rootPath.DIRECTORY_SEPARATOR, '', $filename);
|
||||||
|
|
||||||
@@ -149,6 +150,8 @@ final class TestCaseFactory
|
|||||||
|
|
||||||
$attributesCode = Attributes::code($this->attributes);
|
$attributesCode = Attributes::code($this->attributes);
|
||||||
|
|
||||||
|
$filenameLiteral = var_export($realpath, true);
|
||||||
|
|
||||||
$methodsCode = implode('', array_map(
|
$methodsCode = implode('', array_map(
|
||||||
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
|
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
|
||||||
$methods
|
$methods
|
||||||
@@ -166,7 +169,7 @@ final class TestCaseFactory
|
|||||||
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
||||||
$traitsCode
|
$traitsCode
|
||||||
|
|
||||||
public static \$__filename = '$filename';
|
public static \$__filename = $filenameLiteral;
|
||||||
|
|
||||||
$methodsCode
|
$methodsCode
|
||||||
}
|
}
|
||||||
@@ -197,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);
|
||||||
|
|||||||
@@ -241,6 +241,12 @@ 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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +1142,22 @@ 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
|
||||||
*
|
*
|
||||||
@@ -1155,6 +1171,22 @@ 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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ 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
@@ -6,7 +6,7 @@ namespace Pest;
|
|||||||
|
|
||||||
function version(): string
|
function version(): string
|
||||||
{
|
{
|
||||||
return '4.7.3';
|
return '5.0.0-rc.12';
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDirectory(string $file = ''): string
|
function testDirectory(string $file = ''): string
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ final class Cache implements HandlesArguments
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->hasArgument('--parallel', $arguments)) {
|
if (! $this->hasArgument('--parallel', $arguments) && ! $this->hasArgument('--do-not-cache-result', $arguments) && ! $this->hasArgument('--cache-result', $arguments)) {
|
||||||
return $this->pushArgument('--cache-result', $arguments);
|
return $this->pushArgument('--cache-result', $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,11 +50,14 @@ trait HandleArguments
|
|||||||
*/
|
*/
|
||||||
public function popArgument(string $argument, array $arguments): array
|
public function popArgument(string $argument, array $arguments): array
|
||||||
{
|
{
|
||||||
$arguments = array_flip($arguments);
|
$key = array_search($argument, $arguments, true);
|
||||||
|
|
||||||
unset($arguments[$argument]);
|
while ($key !== false) {
|
||||||
|
unset($arguments[$key]);
|
||||||
|
$key = array_search($argument, $arguments, true);
|
||||||
|
}
|
||||||
|
|
||||||
return array_values(array_flip($arguments));
|
return array_values($arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ 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';
|
||||||
@@ -77,11 +79,9 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
return false;
|
return false;
|
||||||
}))];
|
}))];
|
||||||
|
|
||||||
$originals = array_flip($originals);
|
|
||||||
foreach ($arguments as $argument) {
|
foreach ($arguments as $argument) {
|
||||||
unset($originals[$argument]);
|
$originals = $this->popArgument($argument, $originals);
|
||||||
}
|
}
|
||||||
$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);
|
||||||
|
|||||||
@@ -178,13 +178,7 @@ final class Parallel implements HandlesArguments
|
|||||||
{
|
{
|
||||||
$arguments = new ArgvInput;
|
$arguments = new ArgvInput;
|
||||||
|
|
||||||
foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) {
|
return array_any(self::UNSUPPORTED_ARGUMENTS, fn (string|array $unsupportedArgument): bool => $arguments->hasParameterOption($unsupportedArgument));
|
||||||
if ($arguments->hasParameterOption($unsupportedArgument)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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;
|
||||||
@@ -25,11 +24,17 @@ 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;
|
||||||
@@ -344,6 +349,20 @@ 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()),
|
||||||
@@ -381,6 +400,13 @@ final class WrapperRunner implements RunnerInterface
|
|||||||
$testResultSum->testRunnerTriggeredWarningEvents(),
|
$testResultSum->testRunnerTriggeredWarningEvents(),
|
||||||
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
|
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(),
|
||||||
@@ -447,10 +473,33 @@ final class WrapperRunner implements RunnerInterface
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
|
$coverageFiles = [];
|
||||||
foreach ($this->coverageFiles as $coverageFile) {
|
foreach ($this->coverageFiles as $fileInfo) {
|
||||||
$coverageMerger->addCoverageFromFile($coverageFile);
|
$realPath = $fileInfo->getRealPath();
|
||||||
|
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,6 +7,7 @@ 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;
|
||||||
@@ -147,11 +148,20 @@ 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);
|
||||||
|
|||||||
+105
-12
@@ -27,6 +27,13 @@ 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.
|
||||||
*
|
*
|
||||||
@@ -132,7 +139,8 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
|
|||||||
self::$timeBalanced = true;
|
self::$timeBalanced = true;
|
||||||
self::$shardsOutdated = $newTests !== [];
|
self::$shardsOutdated = $newTests !== [];
|
||||||
} else {
|
} else {
|
||||||
$testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? [];
|
$isInCurrentShard = fn (int $key): bool => $key % $total === ($index - 1);
|
||||||
|
$testsToRun = array_values(array_filter($tests, $isInCurrentShard, ARRAY_FILTER_USE_KEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$shard = [
|
self::$shard = [
|
||||||
@@ -146,7 +154,11 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
|
|||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)];
|
$filter = $this->buildFilterArgument($testsToRun);
|
||||||
|
|
||||||
|
$this->ensureFilterLengthIsSafe($filter);
|
||||||
|
|
||||||
|
return [...$arguments, '--filter', $filter];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,15 +199,14 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
|
|||||||
*/
|
*/
|
||||||
private function allTests(array $arguments): array
|
private function allTests(array $arguments): array
|
||||||
{
|
{
|
||||||
$output = (new Process([
|
$command = $this->buildListTestsCommand(
|
||||||
'php',
|
$arguments,
|
||||||
...$this->removeParallelArguments($arguments),
|
TestSuite::getInstance()->testPath,
|
||||||
'--list-tests',
|
);
|
||||||
]))->setTimeout(120)->mustRun()->getOutput();
|
|
||||||
|
|
||||||
preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
|
$output = new Process($command)->setTimeout(120)->mustRun()->getOutput();
|
||||||
|
|
||||||
return array_values(array_unique($matches[1]));
|
return $this->parseListTestsOutput($output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,15 +215,97 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
|
|||||||
*/
|
*/
|
||||||
private function removeParallelArguments(array $arguments): array
|
private function removeParallelArguments(array $arguments): array
|
||||||
{
|
{
|
||||||
return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true));
|
return array_values(array_filter(
|
||||||
|
$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(mixed $testsToRun): string
|
private function buildFilterArgument(array $testsToRun): string
|
||||||
{
|
{
|
||||||
return addslashes(implode('|', $testsToRun));
|
if ($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
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+38
-4
@@ -9,6 +9,7 @@ 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;
|
||||||
@@ -219,8 +220,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
if (! $watchPatterns->isEnabled()) {
|
if (! $watchPatterns->isEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (! $watchPatterns->isLocally()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return ! ($watchPatterns->isLocally() && self::argumentPresent('--ci', $arguments));
|
return ! self::argumentPresent('--ci', $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -251,7 +255,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_starts_with($arg, "$argument=")) {
|
if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,7 +628,14 @@ 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;
|
||||||
@@ -687,7 +698,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);
|
||||||
@@ -1489,6 +1500,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($arguments as $index => $arg) {
|
foreach ($arguments as $index => $arg) {
|
||||||
|
$arg = (string) $arg; // @phpstan-ignore-line
|
||||||
|
|
||||||
if ($arg === '') {
|
if ($arg === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1638,6 +1651,27 @@ 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');
|
||||||
|
|||||||
@@ -1289,13 +1289,7 @@ 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
|
||||||
{
|
{
|
||||||
foreach ($edges as $components) {
|
return array_any($edges, fn (array $components): bool => in_array($component, $components, true));
|
||||||
if (in_array($component, $components, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pruneMissingTests(): void
|
public function pruneMissingTests(): void
|
||||||
|
|||||||
@@ -386,12 +386,6 @@ final class JsModuleGraph
|
|||||||
|
|
||||||
private static function hasViteConfig(string $projectRoot): bool
|
private static function hasViteConfig(string $projectRoot): bool
|
||||||
{
|
{
|
||||||
foreach (self::VITE_CONFIG_NAMES as $name) {
|
return array_any(self::VITE_CONFIG_NAMES, fn (string $name): bool => is_file($projectRoot.DIRECTORY_SEPARATOR.$name));
|
||||||
if (is_file($projectRoot.DIRECTORY_SEPARATOR.$name)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,7 @@ final class TableExtractor
|
|||||||
}
|
}
|
||||||
|
|
||||||
$prefix = strtolower(substr($trimmed, 0, 6));
|
$prefix = strtolower(substr($trimmed, 0, 6));
|
||||||
|
$matched = array_any(self::DML_PREFIXES, fn (string $dml): bool => str_starts_with($prefix, $dml));
|
||||||
$matched = false;
|
|
||||||
foreach (self::DML_PREFIXES as $dml) {
|
|
||||||
if (str_starts_with($prefix, $dml)) {
|
|
||||||
$matched = true;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $matched) {
|
if (! $matched) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ final readonly class TestPaths
|
|||||||
|
|
||||||
$suffix = $directory->suffix();
|
$suffix = $directory->suffix();
|
||||||
|
|
||||||
if ($suffix !== '') {
|
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
|
||||||
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($suite->files() as $file) {
|
foreach ($suite->files() as $file) {
|
||||||
@@ -94,15 +92,7 @@ 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;
|
||||||
|
|||||||
@@ -253,35 +253,17 @@ final class WatchPatterns
|
|||||||
|
|
||||||
private function patternTargetsDotfiles(string $pattern): bool
|
private function patternTargetsDotfiles(string $pattern): bool
|
||||||
{
|
{
|
||||||
foreach (explode('/', str_replace('\\', '/', $pattern)) as $segment) {
|
return array_any(explode('/', str_replace('\\', '/', $pattern)), fn (string $segment): bool => $segment !== '' && $segment[0] === '.');
|
||||||
if ($segment !== '' && $segment[0] === '.') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function touchesVcs(string $file): bool
|
private function touchesVcs(string $file): bool
|
||||||
{
|
{
|
||||||
foreach (explode('/', $file) as $segment) {
|
return array_any(explode('/', $file), fn (string $segment): bool => in_array($segment, self::VCS_DIRS, true));
|
||||||
if (in_array($segment, self::VCS_DIRS, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function touchesDotfile(string $file): bool
|
private function touchesDotfile(string $file): bool
|
||||||
{
|
{
|
||||||
foreach (explode('/', $file) as $segment) {
|
return array_any(explode('/', $file), fn (string $segment): bool => $segment !== '' && $segment[0] === '.');
|
||||||
if ($segment !== '' && $segment[0] === '.') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function excludeMatches(string $exclude, string $file): bool
|
private function excludeMatches(string $exclude, string $file): bool
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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;
|
||||||
|
|
||||||
@@ -95,10 +96,18 @@ final class Coverage
|
|||||||
$codeCoverage = require $reportPath;
|
$codeCoverage = require $reportPath;
|
||||||
unlink($reportPath);
|
unlink($reportPath);
|
||||||
|
|
||||||
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
|
// @phpstan-ignore-next-line
|
||||||
|
if (is_array($codeCoverage)) {
|
||||||
|
$facade = Facade::fromSerializedData($codeCoverage);
|
||||||
|
|
||||||
/** @var Directory<File|Directory> $report */
|
/** @var Directory<File|Directory> $report */
|
||||||
$report = $codeCoverage->getReport();
|
$report = (fn (): Directory => $this->report)->call($facade);
|
||||||
|
} 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) {
|
||||||
|
|||||||
@@ -86,4 +86,17 @@ 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ 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.
|
||||||
*
|
*
|
||||||
@@ -110,6 +118,14 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
Pest Testing Framework 4.7.3.
|
Pest Testing Framework 5.0.0-rc.12.
|
||||||
|
|
||||||
USAGE: pest <file> [options]
|
USAGE: pest <file> [options]
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
--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
|
||||||
@@ -40,11 +41,15 @@
|
|||||||
--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
|
||||||
@@ -53,20 +58,21 @@
|
|||||||
--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 ... Stop after first error, failure, warning, or risky test
|
--stop-on-defect[=[n]] Stop after first (or n-th) error, failure, warning, or risky test
|
||||||
--stop-on-error ..................................... Stop after first error
|
--stop-on-error[=[n]] ..................... Stop after first (or n-th) error
|
||||||
--stop-on-failure ................................. Stop after first failure
|
--stop-on-failure[=[n]] ................. Stop after first (or n-th) failure
|
||||||
--stop-on-warning ................................. Stop after first warning
|
--stop-on-warning[=[n]] ................. Stop after first (or n-th) warning
|
||||||
--stop-on-risky ................................ Stop after first risky test
|
--stop-on-risky[=[n]] ................ Stop after first (or n-th) risky test
|
||||||
--stop-on-deprecation ... Stop after first test that triggered a deprecation
|
--stop-on-deprecation[=[n]] Stop after first (or n-th) test that triggered a deprecation
|
||||||
--stop-on-notice ............. Stop after first test that triggered a notice
|
--stop-on-notice[=[n]] Stop after first (or n-th) test that triggered a notice
|
||||||
--stop-on-skipped ............................ Stop after first skipped test
|
--stop-on-skipped[=[n]] ............ Stop after first (or n-th) skipped test
|
||||||
--stop-on-incomplete ...................... Stop after first incomplete test
|
--stop-on-incomplete[=[n]] ...... Stop after first (or n-th) 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
|
||||||
@@ -90,7 +96,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|no-depends|random|reverse|size
|
--order-by [order] Run tests in order: default|defects|depends|duration-ascending|duration-descending|no-depends|random|reverse|size-ascending|size-descending
|
||||||
--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"
|
||||||
@@ -101,6 +107,7 @@
|
|||||||
--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
|
||||||
@@ -115,6 +122,7 @@
|
|||||||
--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
|
||||||
@@ -125,12 +133,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:
|
||||||
@@ -151,8 +159,10 @@
|
|||||||
--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
|
||||||
--path-coverage .......... Report path coverage in addition to line coverage
|
--branch-coverage ...... Report branch 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 4.7.3.
|
Pest Testing Framework 5.0.0-rc.12.
|
||||||
|
|
||||||
|
|||||||
@@ -517,6 +517,13 @@
|
|||||||
✓ 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
|
||||||
@@ -752,6 +759,13 @@
|
|||||||
✓ 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
|
||||||
@@ -1696,6 +1710,8 @@
|
|||||||
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
|
||||||
@@ -1707,6 +1723,8 @@
|
|||||||
✓ 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
|
||||||
@@ -1715,6 +1733,52 @@
|
|||||||
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
|
||||||
@@ -1752,6 +1816,72 @@
|
|||||||
✓ 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\IsEnabledForRun
|
||||||
|
✓ does not throw when an integer --random-order-seed is passed as a separate argv element
|
||||||
|
✓ still detects --tia when an integer argument is present
|
||||||
|
|
||||||
|
PASS Tests\Unit\Plugins\Tia\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')
|
||||||
|
|
||||||
PASS Tests\Unit\Preset
|
PASS Tests\Unit\Preset
|
||||||
✓ preset invalid name
|
✓ preset invalid name
|
||||||
✓ preset → myFramework
|
✓ preset → myFramework
|
||||||
@@ -1904,6 +2034,7 @@
|
|||||||
✓ 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
|
||||||
@@ -1937,4 +2068,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: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1328 passed (3008 assertions)
|
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1449 passed (3199 assertions)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
|
|
||||||
|
test('pass', function () {
|
||||||
|
expect('user@example.com')->toBeEmail()
|
||||||
|
->and('notanemail')->not->toBeEmail();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failures', function () {
|
||||||
|
expect('notanemail')->toBeEmail();
|
||||||
|
})->throws(ExpectationFailedException::class);
|
||||||
|
|
||||||
|
test('failures with custom message', function () {
|
||||||
|
expect('notanemail')->toBeEmail('oh no!');
|
||||||
|
})->throws(ExpectationFailedException::class, 'oh no!');
|
||||||
|
|
||||||
|
test('failures with default message', function () {
|
||||||
|
expect('notanemail')->toBeEmail();
|
||||||
|
})->throws(ExpectationFailedException::class, 'Failed asserting that notanemail is an email address.');
|
||||||
|
|
||||||
|
test('not failures', function () {
|
||||||
|
expect('user@example.com')->not->toBeEmail();
|
||||||
|
})->throws(ExpectationFailedException::class);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pest\Exceptions\InvalidExpectationValue;
|
||||||
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
|
|
||||||
|
test('failures with wrong type', function () {
|
||||||
|
expect([])->toBeUlid();
|
||||||
|
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].');
|
||||||
|
|
||||||
|
test('pass', function () {
|
||||||
|
expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid();
|
||||||
|
expect('01BX5ZZKBKACTAV9WEVGEMMVRE')->toBeUlid();
|
||||||
|
expect('7ZZZZZZZZZ0000000000000000')->toBeUlid();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failures', function () {
|
||||||
|
expect('foo')->toBeUlid();
|
||||||
|
})->throws(ExpectationFailedException::class);
|
||||||
|
|
||||||
|
test('failures with message', function () {
|
||||||
|
expect('bar')->toBeUlid('oh no!');
|
||||||
|
})->throws(ExpectationFailedException::class, 'oh no!');
|
||||||
|
|
||||||
|
test('not failures', function () {
|
||||||
|
expect('foo')->not->toBeUlid();
|
||||||
|
});
|
||||||
@@ -14,3 +14,17 @@ it('throw expectation failed exception with array argument', function (): void {
|
|||||||
|
|
||||||
$expectation->throwExpectationFailedException('toBe', ['bar']);
|
$expectation->throwExpectationFailedException('toBe', ['bar']);
|
||||||
})->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'.");
|
})->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'.");
|
||||||
|
|
||||||
|
it('does not truncate long string arguments in error message', function (): void {
|
||||||
|
$expectation = new OppositeExpectation(expect('foo'));
|
||||||
|
|
||||||
|
$longMessage = 'Very long error message. Very long error message. Very long error message.';
|
||||||
|
|
||||||
|
$expectation->throwExpectationFailedException('toBe', [$longMessage]);
|
||||||
|
})->throws(ExpectationFailedException::class, 'Very long error message. Very long error message. Very long error message.');
|
||||||
|
|
||||||
|
it('does not truncate custom error message when using not()', function (): void {
|
||||||
|
$longMessage = 'This is a very detailed custom error message that should not be truncated in the output.';
|
||||||
|
|
||||||
|
expect(true)->not()->toBeTrue($longMessage);
|
||||||
|
})->throws(ExpectationFailedException::class, 'This is a very detailed custom error message that should not be truncated in the output.');
|
||||||
|
|||||||
@@ -24,3 +24,27 @@ test('method hasArgument', function (string $argument, bool $expectedResult) {
|
|||||||
['--a', false],
|
['--a', false],
|
||||||
['--undefined-argument', false],
|
['--undefined-argument', false],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
test('popArgument preserves duplicate values when removing a missing argument', function () {
|
||||||
|
$obj = new class
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
$arguments = ['--verbose', '--exclude-group', 'firstGroup', '--exclude-group', 'secondGroup', '--filter=MyTest'];
|
||||||
|
$result = $obj->popArgument('--missingitem', $arguments);
|
||||||
|
|
||||||
|
expect($result)->toBe($arguments);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('popArgument preserves duplicate values when removing an existing argument', function () {
|
||||||
|
$obj = new class
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
$arguments = ['--verbose', '--exclude-group', 'firstGroup', '--exclude-group', 'secondGroup', '--filter=MyTest'];
|
||||||
|
$result = $obj->popArgument('--verbose', $arguments);
|
||||||
|
|
||||||
|
expect($result)->toBe(['--exclude-group', 'firstGroup', '--exclude-group', 'secondGroup', '--filter=MyTest']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,499 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pest\Exceptions\InvalidOption;
|
||||||
|
use Pest\Plugins\Shard;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
|
||||||
|
describe('getShard', function () {
|
||||||
|
it('parses valid shard format', function (string $format, int $expectedIndex, int $expectedTotal) {
|
||||||
|
$input = new ArgvInput(['test', '--shard', $format]);
|
||||||
|
|
||||||
|
$result = Shard::getShard($input);
|
||||||
|
|
||||||
|
expect($result)->toBe([
|
||||||
|
'index' => $expectedIndex,
|
||||||
|
'total' => $expectedTotal,
|
||||||
|
]);
|
||||||
|
})->with([
|
||||||
|
['1/2', 1, 2],
|
||||||
|
['2/2', 2, 2],
|
||||||
|
['1/4', 1, 4],
|
||||||
|
['4/4', 4, 4],
|
||||||
|
['1/10', 1, 10],
|
||||||
|
['10/10', 10, 10],
|
||||||
|
['5/100', 5, 100],
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('throws exception for invalid format', function (array $arguments) {
|
||||||
|
$input = new ArgvInput($arguments);
|
||||||
|
|
||||||
|
Shard::getShard($input);
|
||||||
|
})->with([
|
||||||
|
[['test', '--shard', 'invalid']],
|
||||||
|
[['test', '--shard', '1']],
|
||||||
|
[['test', '--shard', '1/']],
|
||||||
|
[['test', '--shard', '/2']],
|
||||||
|
[['test', '--shard', 'a/b']],
|
||||||
|
[['test', '--shard', '1.5/2']],
|
||||||
|
])->throws(InvalidOption::class);
|
||||||
|
|
||||||
|
it('throws exception for invalid index or total values', function (array $arguments) {
|
||||||
|
$input = new ArgvInput($arguments);
|
||||||
|
|
||||||
|
Shard::getShard($input);
|
||||||
|
})->with([
|
||||||
|
[['test', '--shard', '0/2']],
|
||||||
|
[['test', '--shard', '1/0']],
|
||||||
|
[['test', '--shard', '3/2']],
|
||||||
|
[['test', '--shard', '5/4']],
|
||||||
|
])->throws(InvalidOption::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildFilterArgument', function () {
|
||||||
|
it('generates compact filter for single test', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildFilterArgument');
|
||||||
|
|
||||||
|
$filter = $method->invoke($shard, ['Tests\\Unit\\ExampleTest']);
|
||||||
|
|
||||||
|
expect($filter)->toBe('Tests\\\\Unit\\\\ExampleTest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates compact filter for multiple tests with common prefix', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildFilterArgument');
|
||||||
|
|
||||||
|
$filter = $method->invoke($shard, [
|
||||||
|
'Tests\\Unit\\Foo\\BarTest',
|
||||||
|
'Tests\\Unit\\Foo\\BazTest',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($filter)->toBe('Tests\\\\Unit\\\\Foo\\\\(BarTest|BazTest)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates compact filter for tests with different namespaces', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildFilterArgument');
|
||||||
|
|
||||||
|
$filter = $method->invoke($shard, [
|
||||||
|
'Tests\\Unit\\FooTest',
|
||||||
|
'Tests\\Feature\\BarTest',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($filter)->toBe('Tests\\\\(Unit\\\\FooTest|Feature\\\\BarTest)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for empty test list', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildFilterArgument');
|
||||||
|
|
||||||
|
$filter = $method->invoke($shard, []);
|
||||||
|
|
||||||
|
expect($filter)->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates compact filter for deeply nested namespaces', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildFilterArgument');
|
||||||
|
|
||||||
|
$filter = $method->invoke($shard, [
|
||||||
|
'Tests\\Unit\\Plugins\\Concerns\\Foo',
|
||||||
|
'Tests\\Unit\\Plugins\\Concerns\\Bar',
|
||||||
|
'Tests\\Unit\\Plugins\\Concerns\\Baz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($filter)->toBe('Tests\\\\Unit\\\\Plugins\\\\Concerns\\\\(Foo|Bar|Baz)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles mix of nested and flat namespaces', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildFilterArgument');
|
||||||
|
|
||||||
|
$tests = [
|
||||||
|
'Tests\\Unit\\SimpleTest',
|
||||||
|
'Tests\\Unit\\Plugins\\Concerns\\HandleArguments',
|
||||||
|
'Tests\\Unit\\Plugins\\Concerns\\Validation',
|
||||||
|
'Tests\\Unit\\Another\\Deep\\Nested\\Test',
|
||||||
|
];
|
||||||
|
|
||||||
|
$filter = $method->invoke($shard, $tests);
|
||||||
|
|
||||||
|
expect($filter)
|
||||||
|
->toBe(addslashes('Tests\\Unit\\(SimpleTest|Plugins\\Concerns\\(HandleArguments|Validation)|Another\\Deep\\Nested\\Test)'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ensureFilterLengthIsSafe', function () {
|
||||||
|
it('accepts filter within length limit', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('ensureFilterLengthIsSafe');
|
||||||
|
|
||||||
|
$filter = str_repeat('a', 1000);
|
||||||
|
|
||||||
|
$method->invoke($shard, $filter);
|
||||||
|
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws exception when filter exceeds default limit', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('ensureFilterLengthIsSafe');
|
||||||
|
|
||||||
|
$filter = str_repeat('a', 32769);
|
||||||
|
|
||||||
|
$method->invoke($shard, $filter);
|
||||||
|
})->throws(InvalidOption::class, 'The generated filter for this shard is too long');
|
||||||
|
|
||||||
|
it('respects custom limit from environment variable', function () {
|
||||||
|
putenv('PEST_SHARD_MAX_FILTER_LENGTH=1000');
|
||||||
|
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('ensureFilterLengthIsSafe');
|
||||||
|
|
||||||
|
$filter = str_repeat('a', 1001);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$method->invoke($shard, $filter);
|
||||||
|
expect(false)->toBeTrue('Should have thrown exception');
|
||||||
|
} catch (InvalidOption $e) {
|
||||||
|
expect($e->getMessage())->toContain('1001 characters')
|
||||||
|
->and($e->getMessage())->toContain('limit is 1000 characters');
|
||||||
|
} finally {
|
||||||
|
putenv('PEST_SHARD_MAX_FILTER_LENGTH');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts filter within custom limit', function () {
|
||||||
|
putenv('PEST_SHARD_MAX_FILTER_LENGTH=1000');
|
||||||
|
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('ensureFilterLengthIsSafe');
|
||||||
|
|
||||||
|
$filter = str_repeat('a', 999);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$method->invoke($shard, $filter);
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
} catch (InvalidOption) {
|
||||||
|
expect(false)->toBeTrue('Should not have thrown exception');
|
||||||
|
} finally {
|
||||||
|
putenv('PEST_SHARD_MAX_FILTER_LENGTH');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleArguments', function () {
|
||||||
|
it('returns original arguments when shard option is not present', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$arguments = ['bin/pest', 'tests/', '--parallel'];
|
||||||
|
|
||||||
|
$result = $shard->handleArguments($arguments);
|
||||||
|
|
||||||
|
expect($result)->toBe($arguments);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes parallel arguments from test discovery', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('removeParallelArguments');
|
||||||
|
|
||||||
|
$arguments = ['bin/pest', '--parallel', '--processes=4', 'tests/', '-p'];
|
||||||
|
|
||||||
|
$result = $method->invoke($shard, $arguments);
|
||||||
|
|
||||||
|
expect($result)->toBe(['bin/pest', 'tests/']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseListTestsOutput', function () {
|
||||||
|
it('parses Tests\\ namespaced classes from --list-tests output', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('parseListTestsOutput');
|
||||||
|
|
||||||
|
$listOutput = <<<'OUT'
|
||||||
|
INFO Available tests:
|
||||||
|
|
||||||
|
- P\Tests\Features\After::__pest_evaluable_it_runs
|
||||||
|
- P\Tests\Features\After::__pest_evaluable_it_runs_twice
|
||||||
|
- P\Tests\Unit\Foo::test_bar
|
||||||
|
OUT;
|
||||||
|
|
||||||
|
expect($method->invoke($shard, $listOutput))->toBe([
|
||||||
|
'Tests\\Features\\After',
|
||||||
|
'Tests\\Unit\\Foo',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates repeated class names from multiple test methods', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('parseListTestsOutput');
|
||||||
|
|
||||||
|
$listOutput = <<<'OUT'
|
||||||
|
- P\Tests\Same::method_a
|
||||||
|
- P\Tests\Same::method_b
|
||||||
|
- P\Tests\Same::method_c
|
||||||
|
OUT;
|
||||||
|
|
||||||
|
expect($method->invoke($shard, $listOutput))->toBe(['Tests\\Same']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty list for output with no matching lines', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('parseListTestsOutput');
|
||||||
|
|
||||||
|
expect($method->invoke($shard, ''))->toBe([])
|
||||||
|
->and($method->invoke($shard, 'some random text'))->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses non-Tests namespaced classes', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('parseListTestsOutput');
|
||||||
|
|
||||||
|
$listOutput = <<<'OUT'
|
||||||
|
- P\Acme\Sharding\OneTest::test_foo
|
||||||
|
- P\Acme\Sharding\TwoTest::test_bar
|
||||||
|
- App\Suite\BazTest::test_qux
|
||||||
|
OUT;
|
||||||
|
|
||||||
|
expect($method->invoke($shard, $listOutput))->toBe([
|
||||||
|
'Acme\\Sharding\\OneTest',
|
||||||
|
'Acme\\Sharding\\TwoTest',
|
||||||
|
'App\\Suite\\BazTest',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses unnamespaced top-level classes', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('parseListTestsOutput');
|
||||||
|
|
||||||
|
expect($method->invoke($shard, ' - P\FooTest::test_bar'))->toBe(['FooTest']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips the P\\ Pest prefix but keeps the rest of the FQCN', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('parseListTestsOutput');
|
||||||
|
|
||||||
|
$listOutput = <<<'OUT'
|
||||||
|
- P\Acme\OneTest::a
|
||||||
|
- Acme\TwoTest::b
|
||||||
|
OUT;
|
||||||
|
|
||||||
|
expect($method->invoke($shard, $listOutput))->toBe([
|
||||||
|
'Acme\\OneTest',
|
||||||
|
'Acme\\TwoTest',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores junk lines that lack the " - …::" framing', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('parseListTestsOutput');
|
||||||
|
|
||||||
|
$listOutput = <<<'OUT'
|
||||||
|
INFO Available tests:
|
||||||
|
|
||||||
|
There were errors:
|
||||||
|
garbage ::: not a test
|
||||||
|
- P\Acme\RealTest::method
|
||||||
|
OUT;
|
||||||
|
|
||||||
|
expect($method->invoke($shard, $listOutput))->toBe(['Acme\\RealTest']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildListTestsCommand', function () {
|
||||||
|
it('builds the list-tests command with the forwarded --test-directory', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildListTestsCommand');
|
||||||
|
|
||||||
|
$command = $method->invoke($shard, ['bin/pest', '--update-shards'], 'custom/suite');
|
||||||
|
|
||||||
|
expect($command)->toBe([
|
||||||
|
'php',
|
||||||
|
'bin/pest',
|
||||||
|
'--update-shards',
|
||||||
|
'--test-directory=custom/suite',
|
||||||
|
'--list-tests',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips --parallel and -p when building the list-tests command', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildListTestsCommand');
|
||||||
|
|
||||||
|
$command = $method->invoke($shard, ['bin/pest', '--parallel', '--update-shards', '-p'], 'tests');
|
||||||
|
|
||||||
|
expect($command)->toBe([
|
||||||
|
'php',
|
||||||
|
'bin/pest',
|
||||||
|
'--update-shards',
|
||||||
|
'--test-directory=tests',
|
||||||
|
'--list-tests',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards --test-directory even when input arguments include one', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildListTestsCommand');
|
||||||
|
|
||||||
|
$command = $method->invoke($shard, ['bin/pest'], 'suites');
|
||||||
|
|
||||||
|
expect($command)->toContain('--test-directory=suites');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips --processes=N when building the list-tests command', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildListTestsCommand');
|
||||||
|
|
||||||
|
$command = $method->invoke($shard, ['bin/pest', '--parallel', '--processes=4', '--update-shards'], 'tests');
|
||||||
|
|
||||||
|
expect($command)->toBe([
|
||||||
|
'php',
|
||||||
|
'bin/pest',
|
||||||
|
'--update-shards',
|
||||||
|
'--test-directory=tests',
|
||||||
|
'--list-tests',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips --processes N (space-separated) when building the list-tests command', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$method = $reflection->getMethod('buildListTestsCommand');
|
||||||
|
|
||||||
|
$command = $method->invoke($shard, ['bin/pest', '--parallel', '--processes', '4', '--update-shards'], 'tests');
|
||||||
|
|
||||||
|
expect($command)->not->toContain('--processes')
|
||||||
|
->and($command)->toContain('--update-shards')
|
||||||
|
->and($command)->toContain('--test-directory=tests');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addOutput', function () {
|
||||||
|
it('displays shard information after test execution', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$property = $reflection->getProperty('shard');
|
||||||
|
$property->setValue(null, [
|
||||||
|
'index' => 2,
|
||||||
|
'total' => 4,
|
||||||
|
'testsRan' => 25,
|
||||||
|
'testsCount' => 100,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$exitCode = $shard->addOutput(0);
|
||||||
|
$outputText = $output->fetch();
|
||||||
|
|
||||||
|
expect($exitCode)->toBe(0)
|
||||||
|
->and($outputText)->toContain('Shard:')
|
||||||
|
->and($outputText)->toContain('2 of 4')
|
||||||
|
->and($outputText)->toContain('25 files ran')
|
||||||
|
->and($outputText)->toContain('out of 100');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses singular form for single test file', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$property = $reflection->getProperty('shard');
|
||||||
|
$property->setValue(null, [
|
||||||
|
'index' => 1,
|
||||||
|
'total' => 4,
|
||||||
|
'testsRan' => 1,
|
||||||
|
'testsCount' => 100,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shard->addOutput(0);
|
||||||
|
$outputText = $output->fetch();
|
||||||
|
|
||||||
|
expect($outputText)->toContain('1 file ran')
|
||||||
|
->and($outputText)->not->toContain('1 files');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns original exit code when shard is not set', function () {
|
||||||
|
$output = new BufferedOutput;
|
||||||
|
$shard = new Shard($output);
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($shard);
|
||||||
|
$property = $reflection->getProperty('shard');
|
||||||
|
$property->setValue(null, null);
|
||||||
|
|
||||||
|
$exitCode = $shard->addOutput(1);
|
||||||
|
$outputText = $output->fetch();
|
||||||
|
|
||||||
|
expect($exitCode)->toBe(1)
|
||||||
|
->and($outputText)->not->toContain('Shard:');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pest\Plugins\Tia;
|
||||||
|
|
||||||
|
test('does not throw when an integer --random-order-seed is passed as a separate argv element', function () {
|
||||||
|
// Mirrors the parallel worker argv (see bin/worker.php), where
|
||||||
|
// `--random-order-seed` and its value arrive as separate items and the
|
||||||
|
// seed value is an int rather than a string. Regression test for the
|
||||||
|
// str_starts_with() TypeError in Tia::argumentPresent() — the same class
|
||||||
|
// of bug as #1206, which was only fixed in HandleArguments.
|
||||||
|
$arguments = ['--order-by=random', '--random-order-seed', 1782350398];
|
||||||
|
|
||||||
|
expect(Tia::isEnabledForRun($arguments))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('still detects --tia when an integer argument is present', function () {
|
||||||
|
$arguments = ['--tia', '--random-order-seed', 1782350398];
|
||||||
|
|
||||||
|
expect(Tia::isEnabledForRun($arguments))->toBeTrue();
|
||||||
|
});
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Process\ExecutableFinder;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
function tiaViteHelperPath(): string
|
||||||
|
{
|
||||||
|
return dirname(__DIR__, 4).'/bin/pest-tia-vite-deps.mjs';
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiaJson(mixed $value): string
|
||||||
|
{
|
||||||
|
return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiaStripFixtures(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'plain-object' => ['{"a":1,"b":2}', ['a' => 1, 'b' => 2]],
|
||||||
|
'plain-array' => ['[1,2,3]', [1, 2, 3]],
|
||||||
|
'nested' => ['{"a":{"b":[1,2]}}', ['a' => ['b' => [1, 2]]]],
|
||||||
|
'empty-object' => ['{}', []],
|
||||||
|
'empty-array' => ['[]', []],
|
||||||
|
|
||||||
|
'line-comment-own-line' => ["{\n// comment\n\"a\":1\n}", ['a' => 1]],
|
||||||
|
'line-comment-trailing' => ["{\"a\":1 // trailing\n}", ['a' => 1]],
|
||||||
|
'leading-line-comments' => ["// a\n// b\n{\"a\":1}", ['a' => 1]],
|
||||||
|
'line-comment-eof-no-newline' => ['{"a":1}// end', ['a' => 1]],
|
||||||
|
|
||||||
|
'block-comment-leading' => ['{/* c */"a":1}', ['a' => 1]],
|
||||||
|
'block-comment-inline' => ['{"a":1,/* c */"b":2}', ['a' => 1, 'b' => 2]],
|
||||||
|
'block-comment-multiline' => ["{\n/* line1\nline2 */\n\"a\":1\n}", ['a' => 1]],
|
||||||
|
'block-comment-eof' => ['{"a":1}/* end */', ['a' => 1]],
|
||||||
|
'jsdoc-style-block' => ["{\n/**\n * doc\n */\n\"a\":1\n}", ['a' => 1]],
|
||||||
|
'block-contains-double-slash' => ['{/* // inside */"a":1}', ['a' => 1]],
|
||||||
|
'line-then-block' => ["{// l\n/* b */\"a\":1}", ['a' => 1]],
|
||||||
|
'comment-with-quotes' => ['{/* "b":2 */"a":1}', ['a' => 1]],
|
||||||
|
'comment-with-braces-commas' => ['{"a":1 /* },{ */,"b":2}', ['a' => 1, 'b' => 2]],
|
||||||
|
|
||||||
|
'trailing-comma-object' => ['{"a":1,}', ['a' => 1]],
|
||||||
|
'trailing-comma-array' => ['[1,2,]', [1, 2]],
|
||||||
|
'trailing-comma-nested' => ['{"a":[1,2,],"b":{"c":3,},}', ['a' => [1, 2], 'b' => ['c' => 3]]],
|
||||||
|
'trailing-comma-newline' => ["{\n\"a\":1,\n}", ['a' => 1]],
|
||||||
|
'trailing-comma-then-block' => ['{"a":1, /* x */ }', ['a' => 1]],
|
||||||
|
'trailing-comma-then-line' => ["{\"a\":1, // x\n}", ['a' => 1]],
|
||||||
|
|
||||||
|
'crlf-line-comment' => ["{\r\n\"a\":1 // c\r\n}", ['a' => 1]],
|
||||||
|
'tabs-whitespace' => ["{\n\t\"a\":\t1\n}", ['a' => 1]],
|
||||||
|
|
||||||
|
'url-in-string' => [tiaJson(['u' => 'https://example.com//x']), ['u' => 'https://example.com//x']],
|
||||||
|
'glob-in-string' => [tiaJson(['g' => 'resources/js/**/*.ts']), ['g' => 'resources/js/**/*.ts']],
|
||||||
|
'path-alias-in-string' => [tiaJson(['p' => '@/*']), ['p' => '@/*']],
|
||||||
|
'block-open-in-string' => [tiaJson(['s' => 'a/*b']), ['s' => 'a/*b']],
|
||||||
|
'block-close-in-string' => [tiaJson(['s' => 'b*/c']), ['s' => 'b*/c']],
|
||||||
|
'block-both-in-string' => [tiaJson(['s' => 'x/*y*/z']), ['s' => 'x/*y*/z']],
|
||||||
|
'star-in-string' => [tiaJson(['s' => '5 * 3']), ['s' => '5 * 3']],
|
||||||
|
'single-slashes-in-string' => [tiaJson(['s' => 'a/b/c']), ['s' => 'a/b/c']],
|
||||||
|
|
||||||
|
'escaped-quote-in-string' => [tiaJson(['s' => 'a"b']), ['s' => 'a"b']],
|
||||||
|
'escaped-backslash-in-string' => [tiaJson(['s' => 'a\\']), ['s' => 'a\\']],
|
||||||
|
'backslash-then-slash' => [tiaJson(['s' => 'a\\/b']), ['s' => 'a\\/b']],
|
||||||
|
'newline-escape-in-string' => [tiaJson(['s' => "a\nb"]), ['s' => "a\nb"]],
|
||||||
|
'unicode-in-string' => [tiaJson(['s' => 'café ☕']), ['s' => 'café ☕']],
|
||||||
|
|
||||||
|
'block-secret' => ['{"a":1/*SECRET*/}', ['a' => 1]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiaStripResults(): array
|
||||||
|
{
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) {
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [];
|
||||||
|
foreach (tiaStripFixtures() as $name => [$raw]) {
|
||||||
|
$payload[] = ['name' => $name, 'raw' => $raw];
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputFile = tempnam(sys_get_temp_dir(), 'tia-strip-');
|
||||||
|
file_put_contents($inputFile, json_encode($payload));
|
||||||
|
|
||||||
|
$helper = str_replace('\\', '/', tiaViteHelperPath());
|
||||||
|
$input = str_replace('\\', '/', $inputFile);
|
||||||
|
|
||||||
|
$script = <<<JS
|
||||||
|
import { stripJsonComments } from '{$helper}'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
const cases = JSON.parse(readFileSync('{$input}', 'utf8'))
|
||||||
|
const out = {}
|
||||||
|
for (const c of cases) {
|
||||||
|
const stripped = stripJsonComments(c.raw)
|
||||||
|
const entry = { stripped }
|
||||||
|
try { entry.parsed = JSON.parse(stripped); entry.ok = true }
|
||||||
|
catch (e) { entry.ok = false; entry.error = String(e && e.message ? e.message : e) }
|
||||||
|
out[c.name] = entry
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(out))
|
||||||
|
JS;
|
||||||
|
|
||||||
|
$process = new Process(['node', '--input-type=module', '-e', $script]);
|
||||||
|
$process->mustRun();
|
||||||
|
|
||||||
|
@unlink($inputFile);
|
||||||
|
|
||||||
|
return $cache = json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiaAliasFixtures(): array
|
||||||
|
{
|
||||||
|
$config = fn (array $compilerOptions): array => ['tsconfig.json' => tiaJson(['compilerOptions' => $compilerOptions])];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'basic' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => ['@/*' => ['./resources/js/*']]]),
|
||||||
|
['@' => 'resources/js'],
|
||||||
|
],
|
||||||
|
'multiple-aliases' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => ['@/*' => ['./resources/js/*'], '~/*' => ['./src/*']]]),
|
||||||
|
['@' => 'resources/js', '~' => 'src'],
|
||||||
|
],
|
||||||
|
'default-baseUrl' => [
|
||||||
|
$config(['paths' => ['@/*' => ['./resources/js/*']]]),
|
||||||
|
['@' => 'resources/js'],
|
||||||
|
],
|
||||||
|
'baseUrl-subdir' => [
|
||||||
|
$config(['baseUrl' => 'src', 'paths' => ['@/*' => ['lib/*']]]),
|
||||||
|
['@' => 'src/lib'],
|
||||||
|
],
|
||||||
|
'jsconfig-fallback' => [
|
||||||
|
['jsconfig.json' => tiaJson(['compilerOptions' => ['baseUrl' => '.', 'paths' => ['@/*' => ['./resources/js/*']]]])],
|
||||||
|
['@' => 'resources/js'],
|
||||||
|
],
|
||||||
|
'first-target-wins' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => ['@/*' => ['./a/*', './b/*']]]),
|
||||||
|
['@' => 'a'],
|
||||||
|
],
|
||||||
|
'multiple-keys-mixed' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => [
|
||||||
|
'@/*' => ['./resources/js/*'],
|
||||||
|
'bad' => ['./x/*'],
|
||||||
|
'~/*' => ['./nope'],
|
||||||
|
]]),
|
||||||
|
['@' => 'resources/js'],
|
||||||
|
],
|
||||||
|
'no-paths' => [
|
||||||
|
$config(['baseUrl' => '.']),
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'no-compiler-options' => [
|
||||||
|
['tsconfig.json' => '{}'],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'key-without-glob' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => ['@' => ['./resources/js/*']]]),
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'target-without-glob' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => ['@/*' => ['./resources/js']]]),
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'target-not-array' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => ['@/*' => './resources/js/*']]),
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'target-empty-array' => [
|
||||||
|
$config(['baseUrl' => '.', 'paths' => ['@/*' => []]]),
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'commented-tsconfig' => [
|
||||||
|
['tsconfig.json' => <<<'JSON'
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more. */
|
||||||
|
"target": "ESNext", // language target
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./resources/js/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
JSON],
|
||||||
|
['@' => 'resources/js'],
|
||||||
|
],
|
||||||
|
'trailing-comma-paths' => [
|
||||||
|
['tsconfig.json' => '{"compilerOptions":{"baseUrl":".","paths":{"@/*":["./resources/js/*",],},},}'],
|
||||||
|
['@' => 'resources/js'],
|
||||||
|
],
|
||||||
|
'malformed-json' => [
|
||||||
|
['tsconfig.json' => '{ this is not json'],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'missing-config' => [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
'tsconfig-precedence' => [
|
||||||
|
[
|
||||||
|
'tsconfig.json' => tiaJson(['compilerOptions' => ['baseUrl' => '.', 'paths' => ['@/*' => ['./a/*']]]]),
|
||||||
|
'jsconfig.json' => tiaJson(['compilerOptions' => ['baseUrl' => '.', 'paths' => ['@/*' => ['./b/*']]]]),
|
||||||
|
],
|
||||||
|
['@' => 'a'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiaAliasResults(): array
|
||||||
|
{
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) {
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roots = [];
|
||||||
|
$written = [];
|
||||||
|
$payload = [];
|
||||||
|
|
||||||
|
foreach (tiaAliasFixtures() as $name => [$files]) {
|
||||||
|
$root = sys_get_temp_dir().'/pest-tia-'.bin2hex(random_bytes(6));
|
||||||
|
mkdir($root, 0755, true);
|
||||||
|
foreach ($files as $fname => $content) {
|
||||||
|
file_put_contents($root.'/'.$fname, $content);
|
||||||
|
$written[] = $root.'/'.$fname;
|
||||||
|
}
|
||||||
|
$root = str_replace('\\', '/', $root);
|
||||||
|
$roots[$name] = $root;
|
||||||
|
$payload[] = ['name' => $name, 'root' => $root];
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputFile = tempnam(sys_get_temp_dir(), 'tia-alias-');
|
||||||
|
file_put_contents($inputFile, json_encode($payload));
|
||||||
|
|
||||||
|
$helper = str_replace('\\', '/', tiaViteHelperPath());
|
||||||
|
$input = str_replace('\\', '/', $inputFile);
|
||||||
|
|
||||||
|
$script = <<<JS
|
||||||
|
import { loadAliasFromTsconfig } from '{$helper}'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
const cases = JSON.parse(readFileSync('{$input}', 'utf8'))
|
||||||
|
const out = {}
|
||||||
|
for (const c of cases) out[c.name] = await loadAliasFromTsconfig(c.root)
|
||||||
|
process.stdout.write(JSON.stringify(out))
|
||||||
|
JS;
|
||||||
|
|
||||||
|
$process = new Process(['node', '--input-type=module', '-e', $script]);
|
||||||
|
$process->mustRun();
|
||||||
|
|
||||||
|
$aliases = json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
@unlink($inputFile);
|
||||||
|
foreach ($written as $f) {
|
||||||
|
@unlink($f);
|
||||||
|
}
|
||||||
|
foreach ($roots as $r) {
|
||||||
|
@rmdir($r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache = ['roots' => $roots, 'aliases' => $aliases];
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
if ((new ExecutableFinder)->find('node') === null) {
|
||||||
|
$this->markTestSkipped('node is not available.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips JSONC down to something JSON.parse accepts', function (string $name) {
|
||||||
|
$result = tiaStripResults()[$name];
|
||||||
|
[, $expected] = tiaStripFixtures()[$name];
|
||||||
|
|
||||||
|
expect($result['ok'])->toBeTrue(
|
||||||
|
"[{$name}] did not parse after stripping: ".($result['error'] ?? 'unknown').PHP_EOL.
|
||||||
|
'stripped: '.$result['stripped'],
|
||||||
|
);
|
||||||
|
expect($result['parsed'])->toEqual($expected);
|
||||||
|
})->with(array_keys(tiaStripFixtures()));
|
||||||
|
|
||||||
|
it('never touches comment-looking sequences inside string values', function () {
|
||||||
|
expect(tiaStripResults()['url-in-string']['stripped'])->toContain('https://example.com//x')
|
||||||
|
->and(tiaStripResults()['glob-in-string']['stripped'])->toContain('resources/js/**/*.ts')
|
||||||
|
->and(tiaStripResults()['block-both-in-string']['stripped'])->toContain('x/*y*/z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the comment body entirely', function () {
|
||||||
|
expect(tiaStripResults()['block-secret']['stripped'])->not->toContain('SECRET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds the expected alias map from a tsconfig', function (string $name) {
|
||||||
|
$results = tiaAliasResults();
|
||||||
|
[, $expectedRelative] = tiaAliasFixtures()[$name];
|
||||||
|
|
||||||
|
$root = $results['roots'][$name];
|
||||||
|
$expected = [];
|
||||||
|
foreach ($expectedRelative as $key => $relative) {
|
||||||
|
$expected[$key] = $root.'/'.$relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($results['aliases'][$name])->toEqual($expected);
|
||||||
|
})->with(array_keys(tiaAliasFixtures()));
|
||||||
@@ -24,13 +24,13 @@ test('parallel', function () use ($run) {
|
|||||||
$file = file_get_contents(__FILE__);
|
$file = file_get_contents(__FILE__);
|
||||||
$file = preg_replace(
|
$file = preg_replace(
|
||||||
'/\$expected = \'.*?\';/',
|
'/\$expected = \'.*?\';/',
|
||||||
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1312 passed (2957 assertions)';",
|
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1432 passed (3146 assertions)';",
|
||||||
$file,
|
$file,
|
||||||
);
|
);
|
||||||
file_put_contents(__FILE__, $file);
|
file_put_contents(__FILE__, $file);
|
||||||
}
|
}
|
||||||
|
|
||||||
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1312 passed (2957 assertions)';
|
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1432 passed (3146 assertions)';
|
||||||
|
|
||||||
expect($output)
|
expect($output)
|
||||||
->toContain("Tests: {$expected}")
|
->toContain("Tests: {$expected}")
|
||||||
@@ -47,3 +47,14 @@ test('parallel reports invalid datasets as failures', function () use ($run) {
|
|||||||
->toContain('Tests: 1 failed, 1 passed (1 assertions)')
|
->toContain('Tests: 1 failed, 1 passed (1 assertions)')
|
||||||
->toContain('Parallel: 3 processes');
|
->toContain('Parallel: 3 processes');
|
||||||
})->skipOnWindows();
|
})->skipOnWindows();
|
||||||
|
|
||||||
|
test('parallel can have multiple exclude-groups', function () use ($run) {
|
||||||
|
$singleExclude = $run('--exclude-group=integration');
|
||||||
|
$doubleExclude = $run('--exclude-group=integration', '--exclude-group=container');
|
||||||
|
|
||||||
|
preg_match('/(\d+) passed/', $singleExclude, $singleMatch);
|
||||||
|
preg_match('/(\d+) passed/', $doubleExclude, $doubleMatch);
|
||||||
|
|
||||||
|
expect((int) $doubleMatch[1])->toBeLessThan((int) $singleMatch[1]);
|
||||||
|
expect($doubleExclude)->toContain('Parallel: 3 processes');
|
||||||
|
})->skipOnWindows();
|
||||||
|
|||||||
Reference in New Issue
Block a user