mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 10:52:14 +02:00
Compare commits
72 Commits
99cc4e0146
...
5.x
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d5bba93f8 | |||
| 79bc7a8257 | |||
| 40b88b62ef | |||
| e3361bc321 | |||
| fc48c1bd1e | |||
| 92e76eb5ab | |||
| da726beffc | |||
| 4ef12b9aac | |||
| bd22f478b8 | |||
| eeaac34cf6 | |||
| b9b07d8983 | |||
| 6aa7d2f891 | |||
| 4d550cecfd | |||
| 1c21a7647a | |||
| d649de1988 | |||
| 783ca4bcd6 | |||
| ba07497219 | |||
| 34695843b3 | |||
| 1ca021dea6 | |||
| d17be9decd | |||
| b828ddcec7 | |||
| f859bb179d | |||
| 2fc75cfcf0 | |||
| 6cc48f63f8 | |||
| e0419d1328 | |||
| faa6988801 | |||
| c12247fafd | |||
| 29b4452443 | |||
| 1b168aba1c | |||
| 6aabd977cd | |||
| a882543c53 | |||
| c250b9da4f | |||
| 46bc3dc628 | |||
| d3ce498b8a | |||
| e1a4b98b71 | |||
| 9afbcd5c18 | |||
| 75593b6454 | |||
| 89590d6120 | |||
| fb0978c9bf | |||
| a3796daa42 | |||
| e3004db666 | |||
| fcf5c27914 | |||
| 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 |
13
.github/SECURITY.md
vendored
Normal file
13
.github/SECURITY.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in Pest, please report it privately using one of the following channels:
|
||||||
|
|
||||||
|
1. **GitHub Private Vulnerability Reporting** (preferred) — go to the repository's **Security** tab and click **"Report a vulnerability"**. This creates a private advisory visible only to maintainers and provides a structured workflow for triage, fix coordination, and CVE assignment.
|
||||||
|
|
||||||
|
2. **Email** — send the details to Nuno Maduro at **enunomaduro@gmail.com**.
|
||||||
|
|
||||||
|
All security vulnerabilities will be promptly addressed.
|
||||||
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "5.x"
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
18
.github/workflows/static.yml
vendored
18
.github/workflows/static.yml
vendored
@ -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 * * *'
|
||||||
@ -24,16 +24,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
dependency-version: [prefer-lowest, prefer-stable]
|
dependency-version: [prefer-stable]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 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,13 +44,13 @@ 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@v5
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # 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
|
||||||
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
|
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
|
||||||
|
|||||||
15
.github/workflows/tests.yml
vendored
15
.github/workflows/tests.yml
vendored
@ -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,21 +24,18 @@ 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@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
tools: composer:v2
|
tools: composer: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@v5
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<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>
|
||||||
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
|
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
|
||||||
|
<a href="https://youtube.com/@nunomaduro?sub_confirmation=1"><img alt="YouTube Channel Subscribers" src="https://img.shields.io/youtube/channel/subscribers/UCO_hYZF2gb_CyG5sA7ArlGg?style=flat&label=youtube&color=brightgreen"></a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@ -17,21 +17,20 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3.0",
|
"php": "^8.4",
|
||||||
"brianium/paratest": "^7.20.0",
|
"brianium/paratest": "^7.22.4",
|
||||||
"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.24",
|
"phpunit/phpunit": "^13.1.8",
|
||||||
"symfony/process": "^7.4.8|^8.0.8"
|
"symfony/process": "^8.1.0"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"filp/whoops": "<2.18.3",
|
"filp/whoops": "<2.18.3",
|
||||||
"phpunit/phpunit": ">12.5.24",
|
"phpunit/phpunit": ">13.1.8",
|
||||||
"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.9",
|
"mrpunyapal/peststan": "^0.2.10",
|
||||||
"pestphp/pest-dev-tools": "^4.1.0",
|
"laravel/pao": "^1.0.6",
|
||||||
"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.22"
|
"pestphp/pest-plugin-type-coverage": "^5.0.0",
|
||||||
|
"psy/psysh": "^0.12.23"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -238,7 +238,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;
|
||||||
@ -915,15 +915,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')),
|
||||||
);
|
);
|
||||||
@ -1138,8 +1130,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')),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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')),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -197,7 +197,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);
|
||||||
|
|||||||
@ -163,7 +163,7 @@ final class Kernel
|
|||||||
$this->terminate();
|
$this->terminate();
|
||||||
|
|
||||||
if (is_array($error = error_get_last())) {
|
if (is_array($error = error_get_last())) {
|
||||||
if (! in_array($error['type'], [E_ERROR, E_CORE_ERROR], true)) {
|
if (! in_array($error['type'], [E_ERROR, E_COMPILE_ERROR, E_CORE_ERROR], true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
|
use Laravel\Pao\Execution;
|
||||||
use Pest\Support\View;
|
use Pest\Support\View;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
@ -28,6 +29,10 @@ final class KernelDump
|
|||||||
*/
|
*/
|
||||||
public function enable(): void
|
public function enable(): void
|
||||||
{
|
{
|
||||||
|
if (class_exists(Execution::class) && Execution::running()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ob_start(function (string $message): string {
|
ob_start(function (string $message): string {
|
||||||
$this->buffer .= $message;
|
$this->buffer .= $message;
|
||||||
|
|
||||||
@ -68,6 +73,10 @@ final class KernelDump
|
|||||||
|
|
||||||
$type = 'INFO';
|
$type = 'INFO';
|
||||||
|
|
||||||
|
if (is_array($error = error_get_last()) && in_array($error['type'], [E_ERROR, E_COMPILE_ERROR, E_CORE_ERROR], true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isInternalError($this->buffer)) {
|
if ($this->isInternalError($this->buffer)) {
|
||||||
$type = 'ERROR';
|
$type = 'ERROR';
|
||||||
$this->buffer = str_replace(
|
$this->buffer = str_replace(
|
||||||
@ -107,7 +116,6 @@ final class KernelDump
|
|||||||
*/
|
*/
|
||||||
private function isInternalError(string $output): bool
|
private function isInternalError(string $output): bool
|
||||||
{
|
{
|
||||||
return str_contains($output, 'An error occurred inside PHPUnit.')
|
return str_contains($output, 'An error occurred inside PHPUnit.');
|
||||||
|| str_contains($output, 'Fatal error');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.');
|
||||||
@ -954,6 +954,7 @@ final class Expectation
|
|||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
|
|
||||||
if ($exception instanceof Throwable) {
|
if ($exception instanceof Throwable) {
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
expect($e)
|
expect($e)
|
||||||
->toBeInstanceOf($exception::class, $message)
|
->toBeInstanceOf($exception::class, $message)
|
||||||
->and($e->getMessage())->toBe($exceptionMessage ?? $exception->getMessage(), $message);
|
->and($e->getMessage())->toBe($exceptionMessage ?? $exception->getMessage(), $message);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ namespace Pest;
|
|||||||
|
|
||||||
function version(): string
|
function version(): string
|
||||||
{
|
{
|
||||||
return '4.6.3';
|
return '5.0.0-rc.9';
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDirectory(string $file = ''): string
|
function testDirectory(string $file = ''): string
|
||||||
|
|||||||
@ -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;
|
||||||
@ -146,7 +151,6 @@ final class WrapperRunner implements RunnerInterface
|
|||||||
public function run(): int
|
public function run(): int
|
||||||
{
|
{
|
||||||
$directory = dirname(__DIR__);
|
$directory = dirname(__DIR__);
|
||||||
assert($directory !== '');
|
|
||||||
ExcludeList::addDirectory($directory);
|
ExcludeList::addDirectory($directory);
|
||||||
TestResultFacade::init();
|
TestResultFacade::init();
|
||||||
EventFacade::instance()->seal();
|
EventFacade::instance()->seal();
|
||||||
@ -448,10 +452,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,
|
||||||
|
|||||||
@ -187,11 +187,11 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
|
|||||||
*/
|
*/
|
||||||
private function allTests(array $arguments): array
|
private function allTests(array $arguments): array
|
||||||
{
|
{
|
||||||
$output = (new Process([
|
$output = new Process([
|
||||||
'php',
|
'php',
|
||||||
...$this->removeParallelArguments($arguments),
|
...$this->removeParallelArguments($arguments),
|
||||||
'--list-tests',
|
'--list-tests',
|
||||||
]))->setTimeout(120)->mustRun()->getOutput();
|
])->setTimeout(120)->mustRun()->getOutput();
|
||||||
|
|
||||||
preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
|
preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ 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\Panic;
|
use Pest\Panic;
|
||||||
|
use Pest\Plugins\Concerns\HandleArguments;
|
||||||
use Pest\Plugins\Tia\BaselineSync;
|
use Pest\Plugins\Tia\BaselineSync;
|
||||||
use Pest\Plugins\Tia\ChangedFiles;
|
use Pest\Plugins\Tia\ChangedFiles;
|
||||||
use Pest\Plugins\Tia\Contracts\State;
|
use Pest\Plugins\Tia\Contracts\State;
|
||||||
@ -36,7 +37,7 @@ use Symfony\Component\Process\Process;
|
|||||||
*/
|
*/
|
||||||
final class Tia implements AddsOutput, HandlesArguments, Terminable
|
final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||||
{
|
{
|
||||||
use Concerns\HandleArguments;
|
use HandleArguments;
|
||||||
|
|
||||||
private const string OPTION = '--tia';
|
private const string OPTION = '--tia';
|
||||||
|
|
||||||
@ -52,6 +53,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
|
|
||||||
private const string BASELINED_OPTION = '--baselined';
|
private const string BASELINED_OPTION = '--baselined';
|
||||||
|
|
||||||
|
private const string BASELINE_PATH_OPTION = '--baseline';
|
||||||
|
|
||||||
private const string ENV_TIA = 'PEST_TIA';
|
private const string ENV_TIA = 'PEST_TIA';
|
||||||
|
|
||||||
private const string ENV_FILTERED = 'PEST_TIA_FILTERED';
|
private const string ENV_FILTERED = 'PEST_TIA_FILTERED';
|
||||||
@ -138,6 +141,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
|
|
||||||
private bool $filteredMode = false;
|
private bool $filteredMode = false;
|
||||||
|
|
||||||
|
private ?string $driftLabel = null;
|
||||||
|
|
||||||
|
private ?string $driftDetails = null;
|
||||||
|
|
||||||
|
private ?string $freshGraphReason = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OutputInterface $output,
|
private readonly OutputInterface $output,
|
||||||
private readonly Recorder $recorder,
|
private readonly Recorder $recorder,
|
||||||
@ -230,7 +239,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirrors {@see \Pest\Plugins\Concerns\HandleArguments::hasArgument()} for
|
* Mirrors {@see HandleArguments::hasArgument()} for
|
||||||
* use from static contexts — matches both `--flag` and `--flag=value`.
|
* use from static contexts — matches both `--flag` and `--flag=value`.
|
||||||
*
|
*
|
||||||
* @param array<int, string> $arguments
|
* @param array<int, string> $arguments
|
||||||
@ -309,6 +318,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
*/
|
*/
|
||||||
public function handleArguments(array $arguments): array
|
public function handleArguments(array $arguments): array
|
||||||
{
|
{
|
||||||
|
if ($this->hasArgument(self::BASELINE_PATH_OPTION, $arguments)) {
|
||||||
|
$this->output->writeln(Storage::tempDir(TestSuite::getInstance()->rootPath));
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
$isWorker = Parallel::isWorker();
|
$isWorker = Parallel::isWorker();
|
||||||
$recordingGlobal = $isWorker && (string) Parallel::getGlobal(self::RECORDING_GLOBAL) === '1';
|
$recordingGlobal = $isWorker && (string) Parallel::getGlobal(self::RECORDING_GLOBAL) === '1';
|
||||||
$replayingGlobal = $isWorker && (string) Parallel::getGlobal(self::REPLAYING_GLOBAL) === '1';
|
$replayingGlobal = $isWorker && (string) Parallel::getGlobal(self::REPLAYING_GLOBAL) === '1';
|
||||||
@ -322,7 +337,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
&& (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL);
|
&& (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL);
|
||||||
$enabled = ! $disabled && ($cliEnabled || $alwaysEnabled);
|
$enabled = ! $disabled && ($cliEnabled || $alwaysEnabled);
|
||||||
$this->filteredMode = ($this->hasArgument(self::FILTERED_OPTION, $arguments) || self::envFlagEnabled(self::ENV_FILTERED) || $watchPatterns->isFiltered())
|
$this->filteredMode = ($this->hasArgument(self::FILTERED_OPTION, $arguments) || self::envFlagEnabled(self::ENV_FILTERED) || $watchPatterns->isFiltered())
|
||||||
&& ! $this->hasExplicitPathArgument($arguments);
|
&& ! $this->hasExplicitPathArgument($arguments)
|
||||||
|
&& ! $this->coverageReportActive();
|
||||||
$freshRequested = $this->hasArgument(self::FRESH_OPTION, $arguments);
|
$freshRequested = $this->hasArgument(self::FRESH_OPTION, $arguments);
|
||||||
$this->forceRefetch = $this->hasArgument(self::REFETCH_OPTION, $arguments);
|
$this->forceRefetch = $this->hasArgument(self::REFETCH_OPTION, $arguments);
|
||||||
|
|
||||||
@ -557,10 +573,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
if (! Fingerprint::structuralMatches($stored, $current)) {
|
if (! Fingerprint::structuralMatches($stored, $current)) {
|
||||||
$drift = Fingerprint::structuralDrift($stored, $current);
|
$drift = Fingerprint::structuralDrift($stored, $current);
|
||||||
|
|
||||||
$this->renderChild(sprintf(
|
$this->driftLabel = $this->formatStructuralDrift($drift);
|
||||||
'Graph structure outdated (%s).',
|
|
||||||
$this->formatStructuralDrift($drift),
|
|
||||||
));
|
|
||||||
|
|
||||||
if (in_array('composer_lock', $drift, true)) {
|
if (in_array('composer_lock', $drift, true)) {
|
||||||
$branchSha = $graph->recordedAtSha($this->branch);
|
$branchSha = $graph->recordedAtSha($this->branch);
|
||||||
@ -570,7 +583,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
$branchSha,
|
$branchSha,
|
||||||
);
|
);
|
||||||
if ($summary !== '') {
|
if ($summary !== '') {
|
||||||
$this->renderChild($summary);
|
$this->driftDetails = $summary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -611,7 +624,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
private function handleParent(array $arguments, string $projectRoot, bool $forceRebuild): array
|
private function handleParent(array $arguments, string $projectRoot, bool $forceRebuild): array
|
||||||
{
|
{
|
||||||
$this->watchPatterns->useDefaults($projectRoot);
|
$this->watchPatterns->useDefaults($projectRoot);
|
||||||
$this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
|
$this->branch = new ChangedFiles($projectRoot)->currentBranch() ?? 'main';
|
||||||
|
|
||||||
$fingerprint = Fingerprint::compute($projectRoot);
|
$fingerprint = Fingerprint::compute($projectRoot);
|
||||||
$this->startFingerprint = $fingerprint;
|
$this->startFingerprint = $fingerprint;
|
||||||
@ -654,6 +667,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->piggybackCoverage && ! $this->state->exists(self::KEY_COVERAGE_CACHE)) {
|
if ($this->piggybackCoverage && ! $this->state->exists(self::KEY_COVERAGE_CACHE)) {
|
||||||
|
if ($graph instanceof Graph && $this->driftLabel === null) {
|
||||||
|
$this->freshGraphReason = 'recording coverage baseline';
|
||||||
|
}
|
||||||
|
|
||||||
return $this->enterRecordMode($arguments);
|
return $this->enterRecordMode($arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -670,7 +687,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
*/
|
*/
|
||||||
private function handleWorker(array $arguments, string $projectRoot, bool $recordingGlobal, bool $replayingGlobal): array
|
private function handleWorker(array $arguments, string $projectRoot, bool $recordingGlobal, bool $replayingGlobal): array
|
||||||
{
|
{
|
||||||
$this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
|
$this->branch = new ChangedFiles($projectRoot)->currentBranch() ?? 'main';
|
||||||
|
|
||||||
if ($replayingGlobal) {
|
if ($replayingGlobal) {
|
||||||
$this->installWorkerReplay($projectRoot);
|
$this->installWorkerReplay($projectRoot);
|
||||||
@ -919,7 +936,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->renderChild(sprintf(
|
$this->renderChild(sprintf(
|
||||||
'TIA mode enabled / %d affected test file%s%s.',
|
'Experimental TIA mode enabled / %d affected test file%s%s.',
|
||||||
count($affected),
|
count($affected),
|
||||||
count($affected) === 1 ? '' : 's',
|
count($affected) === 1 ? '' : 's',
|
||||||
$reasons === [] ? '' : ' ('.implode(', ', $reasons).')',
|
$reasons === [] ? '' : ' ('.implode(', ', $reasons).')',
|
||||||
@ -980,7 +997,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->output->writeln('');
|
$this->output->writeln('');
|
||||||
$this->renderChild('TIA mode enabled / fresh graph.');
|
$this->renderFreshGraph();
|
||||||
|
|
||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
@ -989,7 +1006,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
$this->recordingActive = true;
|
$this->recordingActive = true;
|
||||||
|
|
||||||
$this->output->writeln('');
|
$this->output->writeln('');
|
||||||
$this->renderChild('TIA mode enabled / fresh graph.');
|
$this->renderFreshGraph();
|
||||||
|
|
||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
@ -1002,11 +1019,32 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
return $arguments;
|
return $arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function renderFreshGraph(): void
|
||||||
|
{
|
||||||
|
$headline = 'Experimental TIA mode enabled / fresh graph';
|
||||||
|
|
||||||
|
if ($this->driftLabel !== null) {
|
||||||
|
$headline .= sprintf(' (%s changed)', $this->driftLabel);
|
||||||
|
} elseif ($this->freshGraphReason !== null) {
|
||||||
|
$headline .= sprintf(' (%s)', $this->freshGraphReason);
|
||||||
|
} else {
|
||||||
|
$headline .= '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderChild($headline);
|
||||||
|
|
||||||
|
if ($this->driftDetails !== null) {
|
||||||
|
foreach (explode(', ', $this->driftDetails) as $detail) {
|
||||||
|
$this->output->writeln(sprintf(' <fg=gray>%s</>', $detail));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function emitCoverageDriverMissing(): void
|
private function emitCoverageDriverMissing(): void
|
||||||
{
|
{
|
||||||
$this->renderBadge('WARN', 'No coverage driver is available — skipped.');
|
$this->output->writeln('');
|
||||||
$this->renderChild('Needs ext-pcov or Xdebug with coverage mode enabled to record the dependency graph.');
|
|
||||||
$this->renderChild('Install or enable one and rerun with --tia.');
|
$this->renderChild('Running in TIA mode, however TIA as skipped as it needs Needs ext-pcov or Xdebug.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1324,7 +1362,16 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
/** @var ResultCollector $collector */
|
/** @var ResultCollector $collector */
|
||||||
$collector = Container::getInstance()->get(ResultCollector::class);
|
$collector = Container::getInstance()->get(ResultCollector::class);
|
||||||
|
|
||||||
foreach ($collector->all() as $testId => $result) {
|
$results = $collector->all();
|
||||||
|
$touchedFiles = [];
|
||||||
|
|
||||||
|
foreach ($results as $testId => $result) {
|
||||||
|
$file = $result['file'] ?? null;
|
||||||
|
|
||||||
|
if (is_string($file) && $file !== '') {
|
||||||
|
$touchedFiles[$file] = true;
|
||||||
|
}
|
||||||
|
|
||||||
$graph->setResult(
|
$graph->setResult(
|
||||||
$this->branch,
|
$this->branch,
|
||||||
$testId,
|
$testId,
|
||||||
@ -1332,10 +1379,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
$result['message'],
|
$result['message'],
|
||||||
$result['time'],
|
$result['time'],
|
||||||
$result['assertions'],
|
$result['assertions'],
|
||||||
$result['file'] ?? null,
|
$file,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
|
||||||
|
|
||||||
$collector->reset();
|
$collector->reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1358,6 +1407,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$touchedFiles = [];
|
||||||
|
|
||||||
foreach ($results as $testId => $result) {
|
foreach ($results as $testId => $result) {
|
||||||
$file = $result['file'] ?? null;
|
$file = $result['file'] ?? null;
|
||||||
|
|
||||||
@ -1365,6 +1416,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
$file = $this->resolveFailedTestFile($testId);
|
$file = $this->resolveFailedTestFile($testId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_string($file) && $file !== '') {
|
||||||
|
$touchedFiles[$file] = true;
|
||||||
|
}
|
||||||
|
|
||||||
$graph->setResult(
|
$graph->setResult(
|
||||||
$this->branch,
|
$this->branch,
|
||||||
$testId,
|
$testId,
|
||||||
@ -1376,6 +1431,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
|
||||||
|
|
||||||
$this->saveGraph($graph);
|
$this->saveGraph($graph);
|
||||||
$collector->reset();
|
$collector->reset();
|
||||||
}
|
}
|
||||||
@ -1541,7 +1598,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (! Fingerprint::structuralMatches($fetched->fingerprint(), $current)) {
|
if (! Fingerprint::structuralMatches($fetched->fingerprint(), $current)) {
|
||||||
$this->renderBadge('WARN', 'Fetched baseline still drifts — discarding.');
|
$this->output->writeln(' <fg=gray> However, baseline still drifts — discarding.</>');
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,11 +109,6 @@ final readonly class BaselineSync
|
|||||||
|
|
||||||
$this->clearCooldown();
|
$this->clearCooldown();
|
||||||
|
|
||||||
$this->renderBadge('INFO', sprintf(
|
|
||||||
'Baseline ready (%s).',
|
|
||||||
$this->formatSize($payload['sizeOnDisk']),
|
|
||||||
));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +239,7 @@ final readonly class BaselineSync
|
|||||||
if (is_file($runCacheDir.DIRECTORY_SEPARATOR.self::GRAPH_ASSET)) {
|
if (is_file($runCacheDir.DIRECTORY_SEPARATOR.self::GRAPH_ASSET)) {
|
||||||
@touch($runCacheDir);
|
@touch($runCacheDir);
|
||||||
|
|
||||||
$this->renderBadge('INFO', sprintf(
|
$this->renderChild(sprintf(
|
||||||
'Using cached baseline from %s (run %s).',
|
'Using cached baseline from %s (run %s).',
|
||||||
$repo,
|
$repo,
|
||||||
$runId,
|
$runId,
|
||||||
@ -312,14 +307,15 @@ final readonly class BaselineSync
|
|||||||
{
|
{
|
||||||
$artifactSize = $this->artifactSize($repo, $runId);
|
$artifactSize = $this->artifactSize($repo, $runId);
|
||||||
|
|
||||||
$this->renderBadge('INFO', $artifactSize !== null
|
$this->output->writeln('');
|
||||||
|
$this->renderChild($artifactSize !== null
|
||||||
? sprintf(
|
? sprintf(
|
||||||
'Fetching baseline (%s) from %s…',
|
'Downloading TIA baseline (%s) from %s…',
|
||||||
$this->formatSize($artifactSize),
|
$this->formatSize($artifactSize),
|
||||||
$repo,
|
$repo,
|
||||||
)
|
)
|
||||||
: sprintf(
|
: sprintf(
|
||||||
'Fetching baseline from %s…',
|
'Downloading TIA baseline from %s…',
|
||||||
$repo,
|
$repo,
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -333,10 +329,11 @@ final readonly class BaselineSync
|
|||||||
$process->start();
|
$process->start();
|
||||||
|
|
||||||
$startedAt = microtime(true);
|
$startedAt = microtime(true);
|
||||||
|
$tick = 0;
|
||||||
|
|
||||||
while ($process->isRunning()) {
|
while ($process->isRunning()) {
|
||||||
$this->renderDownloadProgress($runCacheDir, $artifactSize, $startedAt);
|
$this->renderDownloadProgress($startedAt, $tick++);
|
||||||
usleep(250_000);
|
usleep(120_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
$process->wait();
|
$process->wait();
|
||||||
@ -402,30 +399,18 @@ final readonly class BaselineSync
|
|||||||
return is_numeric($size) ? (int) $size : null;
|
return is_numeric($size) ? (int) $size : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderDownloadProgress(string $dir, ?int $totalBytes, float $startedAt): void
|
private function renderDownloadProgress(float $startedAt, int $tick): void
|
||||||
{
|
{
|
||||||
$current = $this->dirSize($dir);
|
static $frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||||
$elapsed = max(0.001, microtime(true) - $startedAt);
|
|
||||||
$speed = (int) ($current / $elapsed);
|
|
||||||
|
|
||||||
if ($totalBytes !== null && $totalBytes > 0) {
|
$elapsed = max(0.0, microtime(true) - $startedAt);
|
||||||
$percent = min(99, (int) floor(($current / $totalBytes) * 100));
|
$frame = $frames[$tick % count($frames)];
|
||||||
$message = sprintf(
|
|
||||||
' <fg=cyan>Downloading</> %s / %s (%d%%, %s/s)',
|
|
||||||
$this->formatSize($current),
|
|
||||||
$this->formatSize($totalBytes),
|
|
||||||
$percent,
|
|
||||||
$this->formatSize($speed),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$message = sprintf(
|
|
||||||
' <fg=cyan>Downloading</> %s (%s/s)',
|
|
||||||
$this->formatSize($current),
|
|
||||||
$this->formatSize($speed),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->output->write("\r\033[K".$message);
|
$this->output->write(sprintf(
|
||||||
|
"\r\033[K <fg=gray>%s %.1fs elapsed</>",
|
||||||
|
$frame,
|
||||||
|
$elapsed,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function clearProgressLine(): void
|
private function clearProgressLine(): void
|
||||||
|
|||||||
@ -31,6 +31,7 @@ final class CoverageMerger
|
|||||||
$current = self::requireCoverage($reportPath);
|
$current = self::requireCoverage($reportPath);
|
||||||
|
|
||||||
if ($current instanceof CodeCoverage) {
|
if ($current instanceof CodeCoverage) {
|
||||||
|
self::primeUncoveredFiles($current);
|
||||||
$state->write(Tia::KEY_COVERAGE_CACHE, self::compress(serialize($current)));
|
$state->write(Tia::KEY_COVERAGE_CACHE, self::compress(serialize($current)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +53,9 @@ final class CoverageMerger
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self::primeUncoveredFiles($cached);
|
||||||
|
self::primeUncoveredFiles($current);
|
||||||
|
|
||||||
self::stripCurrentTestsFromCached($cached, $current);
|
self::stripCurrentTestsFromCached($cached, $current);
|
||||||
|
|
||||||
$cached->merge($current);
|
$cached->merge($current);
|
||||||
@ -65,6 +69,11 @@ final class CoverageMerger
|
|||||||
$state->write(Tia::KEY_COVERAGE_CACHE, self::compress($serialised));
|
$state->write(Tia::KEY_COVERAGE_CACHE, self::compress($serialised));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function primeUncoveredFiles(CodeCoverage $coverage): void
|
||||||
|
{
|
||||||
|
$coverage->getData(false);
|
||||||
|
}
|
||||||
|
|
||||||
private static function compress(string $bytes): string
|
private static function compress(string $bytes): string
|
||||||
{
|
{
|
||||||
$compressed = @gzencode($bytes);
|
$compressed = @gzencode($bytes);
|
||||||
|
|||||||
@ -4,12 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Plugins\Tia;
|
namespace Pest\Plugins\Tia;
|
||||||
|
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final readonly class Fingerprint
|
final readonly class Fingerprint
|
||||||
{
|
{
|
||||||
private const int SCHEMA_VERSION = 15;
|
private const int SCHEMA_VERSION = 17;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
@ -23,15 +25,15 @@ final readonly class Fingerprint
|
|||||||
'structural' => [
|
'structural' => [
|
||||||
'schema' => self::SCHEMA_VERSION,
|
'schema' => self::SCHEMA_VERSION,
|
||||||
'composer_lock' => self::composerLockHash($projectRoot),
|
'composer_lock' => self::composerLockHash($projectRoot),
|
||||||
'phpunit_xml' => self::hashIfExists($projectRoot.'/phpunit.xml'),
|
'phpunit_xml' => self::trackedHash($projectRoot, 'phpunit.xml'),
|
||||||
'phpunit_xml_dist' => self::hashIfExists($projectRoot.'/phpunit.xml.dist'),
|
'phpunit_xml_dist' => self::trackedHash($projectRoot, 'phpunit.xml.dist'),
|
||||||
'pest_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseFactory.php'),
|
// 'pest_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseFactory.php'),
|
||||||
'pest_method_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseMethodFactory.php'),
|
// 'pest_method_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseMethodFactory.php'),
|
||||||
'vite_config' => self::viteConfigHash($projectRoot),
|
'vite_config' => self::viteConfigHash($projectRoot),
|
||||||
'package_json' => self::packageJsonHash($projectRoot),
|
// 'package_json' => self::packageJsonHash($projectRoot),
|
||||||
'package_lock' => self::packageLockHash($projectRoot),
|
'package_lock' => self::packageLockHash($projectRoot),
|
||||||
'js_config' => self::jsConfigHash($projectRoot),
|
'js_config' => self::jsConfigHash($projectRoot),
|
||||||
'composer_json' => self::composerJsonHash($projectRoot),
|
// 'composer_json' => self::composerJsonHash($projectRoot),
|
||||||
],
|
],
|
||||||
'environmental' => [
|
'environmental' => [
|
||||||
'php_minor' => PHP_MAJOR_VERSION,
|
'php_minor' => PHP_MAJOR_VERSION,
|
||||||
@ -160,6 +162,10 @@ final readonly class Fingerprint
|
|||||||
$parts = [];
|
$parts = [];
|
||||||
|
|
||||||
foreach (JsModuleGraph::VITE_CONFIG_NAMES as $name) {
|
foreach (JsModuleGraph::VITE_CONFIG_NAMES as $name) {
|
||||||
|
if (! self::isTrackedByGit($projectRoot, $name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$hash = self::contentHashOrNull($projectRoot.'/'.$name);
|
$hash = self::contentHashOrNull($projectRoot.'/'.$name);
|
||||||
|
|
||||||
if ($hash !== null) {
|
if ($hash !== null) {
|
||||||
@ -175,6 +181,10 @@ final readonly class Fingerprint
|
|||||||
$parts = [];
|
$parts = [];
|
||||||
|
|
||||||
foreach (['tsconfig.json', 'tsconfig.app.json', 'jsconfig.json'] as $name) {
|
foreach (['tsconfig.json', 'tsconfig.app.json', 'jsconfig.json'] as $name) {
|
||||||
|
if (! self::isTrackedByGit($projectRoot, $name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$hash = self::hashIfExists($projectRoot.'/'.$name);
|
$hash = self::hashIfExists($projectRoot.'/'.$name);
|
||||||
|
|
||||||
if ($hash !== null) {
|
if ($hash !== null) {
|
||||||
@ -185,52 +195,9 @@ final readonly class Fingerprint
|
|||||||
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function packageJsonHash(string $projectRoot): ?string
|
|
||||||
{
|
|
||||||
$path = $projectRoot.'/package.json';
|
|
||||||
|
|
||||||
if (! is_file($path)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$raw = @file_get_contents($path);
|
|
||||||
|
|
||||||
if ($raw === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($raw, true);
|
|
||||||
|
|
||||||
if (! is_array($data)) {
|
|
||||||
$hash = @hash_file('xxh128', $path);
|
|
||||||
|
|
||||||
return $hash === false ? null : $hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
$relevant = [
|
|
||||||
'type' => $data['type'] ?? null,
|
|
||||||
'packageManager' => $data['packageManager'] ?? null,
|
|
||||||
'dependencies' => $data['dependencies'] ?? null,
|
|
||||||
'devDependencies' => $data['devDependencies'] ?? null,
|
|
||||||
'optionalDependencies' => $data['optionalDependencies'] ?? null,
|
|
||||||
'peerDependencies' => $data['peerDependencies'] ?? null,
|
|
||||||
'overrides' => $data['overrides'] ?? null,
|
|
||||||
'resolutions' => $data['resolutions'] ?? null,
|
|
||||||
'imports' => $data['imports'] ?? null,
|
|
||||||
'exports' => $data['exports'] ?? null,
|
|
||||||
'browser' => $data['browser'] ?? null,
|
|
||||||
];
|
|
||||||
|
|
||||||
self::sortRecursively($relevant);
|
|
||||||
|
|
||||||
$json = json_encode($relevant);
|
|
||||||
|
|
||||||
return $json === false ? null : hash('xxh128', $json);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function composerLockHash(string $projectRoot): ?string
|
private static function composerLockHash(string $projectRoot): ?string
|
||||||
{
|
{
|
||||||
return self::hashIfExists($projectRoot.'/composer.lock');
|
return self::trackedHash($projectRoot, 'composer.lock');
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function packageLockHash(string $projectRoot): ?string
|
private static function packageLockHash(string $projectRoot): ?string
|
||||||
@ -238,7 +205,7 @@ final readonly class Fingerprint
|
|||||||
$parts = [];
|
$parts = [];
|
||||||
|
|
||||||
foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) {
|
foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) {
|
||||||
$hash = self::hashIfExists($projectRoot.'/'.$name);
|
$hash = self::trackedHash($projectRoot, $name);
|
||||||
|
|
||||||
if ($hash !== null) {
|
if ($hash !== null) {
|
||||||
$parts[] = $name.':'.$hash;
|
$parts[] = $name.':'.$hash;
|
||||||
@ -248,68 +215,47 @@ final readonly class Fingerprint
|
|||||||
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function composerJsonHash(string $projectRoot): ?string
|
private static function trackedHash(string $projectRoot, string $relativePath): ?string
|
||||||
{
|
{
|
||||||
$path = $projectRoot.'/composer.json';
|
if (! self::isTrackedByGit($projectRoot, $relativePath)) {
|
||||||
|
|
||||||
if (! is_file($path)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw = @file_get_contents($path);
|
return self::hashIfExists($projectRoot.'/'.$relativePath);
|
||||||
|
|
||||||
if ($raw === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($raw, true);
|
|
||||||
|
|
||||||
if (! is_array($data)) {
|
|
||||||
$hash = @hash_file('xxh128', $path);
|
|
||||||
|
|
||||||
return $hash === false ? null : $hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = is_array($data['config'] ?? null) ? $data['config'] : [];
|
|
||||||
$relevantConfig = array_intersect_key($config, [
|
|
||||||
'platform' => true,
|
|
||||||
'allow-plugins' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$relevant = [
|
|
||||||
'autoload' => $data['autoload'] ?? null,
|
|
||||||
'autoload-dev' => $data['autoload-dev'] ?? null,
|
|
||||||
'require' => $data['require'] ?? null,
|
|
||||||
'require-dev' => $data['require-dev'] ?? null,
|
|
||||||
'extra' => $data['extra'] ?? null,
|
|
||||||
'repositories' => $data['repositories'] ?? null,
|
|
||||||
'minimum-stability' => $data['minimum-stability'] ?? null,
|
|
||||||
'prefer-stable' => $data['prefer-stable'] ?? null,
|
|
||||||
'config' => $relevantConfig === [] ? null : $relevantConfig,
|
|
||||||
];
|
|
||||||
|
|
||||||
self::sortRecursively($relevant);
|
|
||||||
|
|
||||||
$json = json_encode($relevant);
|
|
||||||
|
|
||||||
return $json === false ? null : hash('xxh128', $json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function sortRecursively(mixed &$value): void
|
/**
|
||||||
|
* Returns true when the file exists and is not gitignored.
|
||||||
|
*
|
||||||
|
* Gitignored lockfiles (e.g. `package-lock.json` excluded from the repo)
|
||||||
|
* regenerate per-machine with OS-specific optional deps, which would
|
||||||
|
* otherwise force a fingerprint mismatch on every fetched baseline.
|
||||||
|
*/
|
||||||
|
private static function isTrackedByGit(string $projectRoot, string $relativePath): bool
|
||||||
{
|
{
|
||||||
if (! is_array($value)) {
|
if (! is_file($projectRoot.'/'.$relativePath)) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$isAssoc = ! array_is_list($value);
|
static $cache = [];
|
||||||
|
|
||||||
if ($isAssoc) {
|
$key = $projectRoot."\0".$relativePath;
|
||||||
ksort($value);
|
|
||||||
|
if (isset($cache[$key])) {
|
||||||
|
return $cache[$key];
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($value as &$child) {
|
if (! is_dir($projectRoot.'/.git') && ! is_file($projectRoot.'/.git')) {
|
||||||
self::sortRecursively($child);
|
return $cache[$key] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$finder = (new Finder)
|
||||||
|
->in($projectRoot)
|
||||||
|
->depth('== 0')
|
||||||
|
->name($relativePath)
|
||||||
|
->ignoreVCSIgnored(true);
|
||||||
|
|
||||||
|
return $cache[$key] = $finder->hasResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function contentHashOrNull(string $path): ?string
|
private static function contentHashOrNull(string $path): ?string
|
||||||
|
|||||||
@ -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
|
||||||
@ -1321,6 +1315,51 @@ final class Graph
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune baseline result entries whose test files were just executed but whose
|
||||||
|
* test IDs are no longer present (e.g. the test method was removed or renamed).
|
||||||
|
*
|
||||||
|
* @param array<int, string> $touchedFiles Absolute or project-relative paths.
|
||||||
|
* @param array<int, string> $keepTestIds Test IDs that produced a result this run.
|
||||||
|
*/
|
||||||
|
public function pruneStaleResults(string $branch, array $touchedFiles, array $keepTestIds): void
|
||||||
|
{
|
||||||
|
if (! isset($this->baselines[$branch]['results'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$touched = [];
|
||||||
|
foreach ($touchedFiles as $file) {
|
||||||
|
$rel = $this->relative($file);
|
||||||
|
|
||||||
|
if ($rel !== null) {
|
||||||
|
$touched[$rel] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($touched === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keep = array_fill_keys($keepTestIds, true);
|
||||||
|
|
||||||
|
foreach ($this->baselines[$branch]['results'] as $testId => $result) {
|
||||||
|
$file = $result['file'] ?? null;
|
||||||
|
if (! is_string($file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! isset($touched[$file])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($keep[$testId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($this->baselines[$branch]['results'][$testId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function decode(string $json, string $projectRoot): ?self
|
public static function decode(string $json, string $projectRoot): ?self
|
||||||
{
|
{
|
||||||
$data = json_decode($json, true);
|
$data = json_decode($json, true);
|
||||||
|
|||||||
@ -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 [];
|
||||||
|
|||||||
@ -94,15 +94,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
|
||||||
|
|||||||
@ -24,6 +24,9 @@ final class PcovRestarter implements Restarter
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (getenv(self::ENV_RESTARTED) === '1') {
|
if (getenv(self::ENV_RESTARTED) === '1') {
|
||||||
|
putenv(self::ENV_RESTARTED);
|
||||||
|
unset($_ENV[self::ENV_RESTARTED]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
Pest Testing Framework 4.6.3.
|
Pest Testing Framework 5.0.0-rc.9.
|
||||||
|
|
||||||
USAGE: pest <file> [options]
|
USAGE: pest <file> [options]
|
||||||
|
|
||||||
@ -45,6 +45,7 @@
|
|||||||
--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)
|
||||||
|
|
||||||
EXECUTION OPTIONS:
|
EXECUTION OPTIONS:
|
||||||
--parallel ........................................... Run tests in parallel
|
--parallel ........................................... Run tests in parallel
|
||||||
@ -125,12 +126,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:
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
|
|
||||||
Pest Testing Framework 4.6.3.
|
Pest Testing Framework 5.0.0-rc.9.
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,56 @@
|
|||||||
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234']
|
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234']
|
||||||
##teamcity[testCount count='8' flowId='1234']
|
##teamcity[testCount count='8' flowId='1234']
|
||||||
|
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234']
|
||||||
|
##teamcity[testCount count='8' flowId='1234']
|
||||||
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
||||||
|
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
||||||
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
||||||
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
||||||
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
||||||
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
||||||
|
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234']
|
||||||
##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234']
|
||||||
##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234']
|
##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234']
|
||||||
|
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
||||||
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
||||||
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
||||||
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
||||||
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234']
|
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234']
|
||||||
|
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
||||||
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
||||||
##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234']
|
##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234']
|
||||||
|
##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234']
|
||||||
##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234']
|
##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234']
|
||||||
##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
||||||
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
||||||
##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
||||||
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
||||||
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234']
|
||||||
##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234']
|
##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234']
|
||||||
##teamcity[testFinished name='it is passing' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it is passing' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it is passing' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
|
||||||
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
|
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
|
||||||
|
|
||||||
[90mTests:[39m [31;1m3 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m,[39m[39m [39m[32;1m1 passed[39;22m[90m (3 assertions)[39m
|
[90mTests:[39m [31;1m3 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m,[39m[39m [39m[32;1m1 passed[39;22m[90m (3 assertions)[39m
|
||||||
[90mDuration:[39m [39m1.00s[39m
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|
||||||
|
|
||||||
|
[90mTests:[39m [31;1m3 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m,[39m[39m [39m[32;1m1 passed[39;22m[90m (3 assertions)[39m
|
||||||
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,38 @@
|
|||||||
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
|
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
|
||||||
##teamcity[testCount count='4' flowId='1234']
|
##teamcity[testCount count='4' flowId='1234']
|
||||||
|
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
|
||||||
|
##teamcity[testCount count='4' flowId='1234']
|
||||||
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
||||||
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
||||||
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
||||||
|
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
||||||
|
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
||||||
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
||||||
##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
|
##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
|
||||||
|
##teamcity[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
|
||||||
|
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
|
||||||
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
|
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
|
||||||
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
|
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
|
||||||
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
|
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
|
||||||
##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234']
|
##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234']
|
||||||
|
##teamcity[testSuiteStarted name='`block` → can pass with dataset in describe block' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block' flowId='1234']
|
||||||
|
##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234']
|
||||||
##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234']
|
##teamcity[testStarted name='`block` → can pass with dataset in describe block with data set "(1)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::`block` → can pass with dataset in describe block with data set "(1)"' flowId='1234']
|
||||||
##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234']
|
##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testFinished name='`block` → can pass with dataset in describe block with data set "(1)"' duration='100000' flowId='1234']
|
||||||
##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
|
##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
|
||||||
|
##teamcity[testSuiteFinished name='`block` → can pass with dataset in describe block' flowId='1234']
|
||||||
|
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
|
||||||
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
|
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
|
||||||
|
|
||||||
[90mTests:[39m [32;1m4 passed[39;22m[90m (4 assertions)[39m
|
[90mTests:[39m [32;1m4 passed[39;22m[90m (4 assertions)[39m
|
||||||
[90mDuration:[39m [39m1.00s[39m
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|
||||||
|
|
||||||
|
[90mTests:[39m [32;1m4 passed[39;22m[90m (4 assertions)[39m
|
||||||
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
✓ preset → strict → ignoring ['Pest\Plugins\Tia\BaselineSync', 'usleep']
|
✓ preset → strict → ignoring ['Pest\Plugins\Tia\BaselineSync', 'usleep']
|
||||||
✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …]
|
✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …]
|
||||||
✓ globals
|
✓ globals
|
||||||
✓ contracts
|
|
||||||
|
|
||||||
PASS Tests\Environments\Windows
|
PASS Tests\Environments\Windows
|
||||||
✓ global functions are loaded
|
✓ global functions are loaded
|
||||||
@ -1697,6 +1696,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
|
||||||
@ -1716,6 +1717,43 @@
|
|||||||
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\Tia\ContentHash
|
||||||
|
✓ of() → it returns false when file does not exist
|
||||||
|
✓ of() → it hashes an existing file
|
||||||
|
✓ PHP files → it produces the same hash regardless of whitespace differences
|
||||||
|
✓ PHP files → it ignores single-line comments
|
||||||
|
✓ PHP files → it ignores hash-style comments
|
||||||
|
✓ PHP files → it ignores multi-line comments
|
||||||
|
✓ PHP files → it ignores doc comments
|
||||||
|
✓ PHP files → it detects code changes
|
||||||
|
✓ PHP files → it preserves whitespace inside string literals
|
||||||
|
✓ PHP files → it treats variable renames as a change
|
||||||
|
✓ PHP files → it falls back to a raw hash for unparseable PHP
|
||||||
|
✓ PHP files → it is case-insensitive on the file extension
|
||||||
|
✓ Blade files → it strips blade comments
|
||||||
|
✓ Blade files → it strips multi-line blade comments
|
||||||
|
✓ Blade files → it collapses whitespace
|
||||||
|
✓ Blade files → it detects content changes
|
||||||
|
✓ Blade files → it keeps blade directives intact
|
||||||
|
✓ Blade files → it does not use the PHP tokenizer for blade files
|
||||||
|
✓ JavaScript-like files → it strips line comments
|
||||||
|
✓ JavaScript-like files → it strips block comments on their own lines
|
||||||
|
✓ JavaScript-like files → it collapses whitespace
|
||||||
|
✓ JavaScript-like files → it detects code changes
|
||||||
|
✓ JavaScript-like files → it does not strip inline trailing comments
|
||||||
|
✓ JavaScript-like files → it applies the same rules to .ts files
|
||||||
|
✓ JavaScript-like files → it applies the same rules to .tsx files
|
||||||
|
✓ JavaScript-like files → it applies the same rules to .jsx files
|
||||||
|
✓ JavaScript-like files → it applies the same rules to .vue files
|
||||||
|
✓ JavaScript-like files → it applies the same rules to .svelte files
|
||||||
|
✓ JavaScript-like files → it applies the same rules to .mjs, .cjs, and .mts files
|
||||||
|
✓ unknown extensions → it hashes the raw content for unknown extensions
|
||||||
|
✓ unknown extensions → it does not normalise whitespace for unknown extensions
|
||||||
|
✓ unknown extensions → it does not strip comments for unknown extensions
|
||||||
|
✓ unknown extensions → it hashes files with no extension as raw content
|
||||||
|
✓ output format → it returns a 32-character hex xxh128 hash
|
||||||
|
✓ output format → it returns a stable hash for empty content
|
||||||
|
|
||||||
PASS Tests\Unit\Preset
|
PASS Tests\Unit\Preset
|
||||||
✓ preset invalid name
|
✓ preset invalid name
|
||||||
✓ preset → myFramework
|
✓ preset → myFramework
|
||||||
@ -1901,4 +1939,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, 1294 passed (2971 assertions)
|
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1330 passed (3013 assertions)
|
||||||
@ -33,13 +33,3 @@ arch('globals')
|
|||||||
->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep'])
|
->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep'])
|
||||||
->not->toBeUsed()
|
->not->toBeUsed()
|
||||||
->ignoring(Expectation::class);
|
->ignoring(Expectation::class);
|
||||||
|
|
||||||
arch('contracts')
|
|
||||||
->expect('Pest\Contracts')
|
|
||||||
->toOnlyUse([
|
|
||||||
'NunoMaduro\Collision\Contracts',
|
|
||||||
'Pest\Factories\TestCaseMethodFactory',
|
|
||||||
'Symfony\Component\Console',
|
|
||||||
'Pest\Arch\Contracts',
|
|
||||||
'Pest\PendingCalls',
|
|
||||||
])->toBeInterfaces();
|
|
||||||
|
|||||||
@ -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.');
|
||||||
|
|||||||
@ -16,6 +16,7 @@ $run = function () {
|
|||||||
|
|
||||||
test('parallel', function () use ($run) {
|
test('parallel', function () use ($run) {
|
||||||
$output = $run('--exclude-group=integration');
|
$output = $run('--exclude-group=integration');
|
||||||
|
$output = implode("\n", array_slice(explode("\n", $output), -10));
|
||||||
|
|
||||||
if (getenv('REBUILD_SNAPSHOTS')) {
|
if (getenv('REBUILD_SNAPSHOTS')) {
|
||||||
preg_match('/Tests:\s+(.+\(\d+ assertions\))/', $output, $matches);
|
preg_match('/Tests:\s+(.+\(\d+ assertions\))/', $output, $matches);
|
||||||
@ -23,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, 1278 passed (2920 assertions)';",
|
"\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1314 passed (2962 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, 1278 passed (2920 assertions)';
|
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1314 passed (2962 assertions)';
|
||||||
|
|
||||||
expect($output)
|
expect($output)
|
||||||
->toContain("Tests: {$expected}")
|
->toContain("Tests: {$expected}")
|
||||||
|
|||||||
Reference in New Issue
Block a user