Compare commits

...

56 Commits

Author SHA1 Message Date
nuno maduro 5dc49a71d6 release: v4.7.5 2026-07-06 18:06:29 +01:00
nuno maduro 1adb484d3f chore: bumps deps 2026-07-06 18:05:35 +01:00
nuno maduro aadf7e92a6 fix: escape generated test case filename (#1746)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:18:26 +01:00
nuno maduro df42b9ed7b chore: style 2026-07-06 13:17:11 +01:00
dependabot[bot] 632ae79c8b build(deps): bump actions/cache in the github-actions group (#1745)
Bumps the github-actions group with 1 update: [actions/cache](https://github.com/actions/cache).


Updates `actions/cache` from 5.0.5 to 6.1.0
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 12:57:08 +01:00
nuno maduro ee2e97e932 release: v4.7.4 2026-06-25 20:09:05 +01:00
nuno maduro b561236325 chore: bumps phpunit 2026-06-25 20:09:05 +01:00
dependabot[bot] e685179b7e Bump actions/checkout from 6.0.3 to 7.0.0 in the github-actions group (#1737)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 20:03:39 +01:00
Conor Murphy ac8feafdcc fix: guard Tia argv scanning against non-string args (#1736)
The Tia plugin scans the raw argv with str_starts_with() in
argumentPresent() and hasExplicitPathArgument(). In the parallel worker
path (bin/worker.php) the unserialized argv can contain an integer
--random-order-seed value as a separate element, which made
str_starts_with() throw:

    TypeError: str_starts_with(): Argument #1 ($haystack) must be of
    type string, int given

This is the same class of bug as #1206, which was only fixed in
HandleArguments; it resurfaced in the newer Tia plugin. Cast each argv
element to string before scanning, mirroring the #1206 fix.

Adds a regression test, and updates the parallel count assertion and the
success snapshot to account for it.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:03:06 +01:00
nuno maduro 87882a8561 fix: exit code 2026-06-12 06:57:27 +01:00
nuno maduro 77ef7e0df1 ci: bumps setup-php pinned commit 2026-06-12 06:51:58 +01:00
dependabot[bot] 12100dd901 Bump actions/checkout from 6.0.2 to 6.0.3 in the github-actions group (#1730)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 06:51:20 +01:00
nuno maduro 20c12d006e docs: fixes alt 2026-06-12 06:32:20 +01:00
nuno maduro 3aab02d2eb ci: fixes dd test 2026-06-11 10:50:24 +01:00
nuno maduro 553aac65e6 release: v4.7.3 2026-06-11 10:40:18 +01:00
Punyapal Shah be49a3ce18 Fix: dd (#1692)
* fix: update dd method to return never type

Co-authored-by: Copilot <copilot@github.com>

* fix: enhance var_dump calls to accept additional arguments

* fix: update dd method to handle paratest and collision printer environments

* test: add dd method tests for ExpectationFailedException in parallel mode

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
2026-06-11 10:16:07 +01:00
Chetan 04c9d41895 fix: update gitlab urls for issues and prs to match new format (#1728)
* fix: update gitlab urls for issues and prs to match new format

Added an optional host parameter to the gitlab method, defaulting to 'gitlab.com'. Updated the issues and prs URLs to include '/-/' in the path, which is the new format for GitLab URLs. This change ensures that the URLs generated for GitLab projects are correct and reflect the new structure of GitLab's URLs for issues and merge requests.

Host parameter allows users to specify a custom GitLab instance if they are using a self-hosted version of GitLab, while still maintaining the default behavior for users who are using gitlab.com.

* fix: update gitlab method to use hostname parameter correctly
2026-06-11 10:13:26 +01:00
nuno maduro dfb7b870af fix: retry output expected string 2026-06-11 10:12:41 +01:00
nuno maduro 37821e1108 chore: fixes output snapshots on team city 2026-06-11 10:12:32 +01:00
nuno maduro cd711a25d5 chore: bumps phpunit 2026-06-11 10:08:36 +01:00
nuno maduro eee60c9e11 fix: duplicated team city output 2026-06-11 10:08:27 +01:00
jp c40c8dbc24 [4.x] Fix Checks (#1709)
* fix(composer): set root version for feature-branch CI

pest-plugin-browser ^4.3.1 requires pestphp/pest ^4.4.5 on the root package.
Without an explicit version, Composer infers dev-<branch> on PR branches,
which fails composer update before tests run.

* set root version of composer

* fix indent
2026-06-06 01:23:08 +01:00
dependabot[bot] 2d93c9c373 Bump shivammathur/setup-php in the github-actions group (#1707)
Bumps the github-actions group with 1 update: [shivammathur/setup-php](https://github.com/shivammathur/setup-php).


Updates `shivammathur/setup-php` from 2.37.0 to 2.37.1
- [Release notes](https://github.com/shivammathur/setup-php/releases)
- [Commits](https://github.com/shivammathur/setup-php/compare/accd6127cb78bee3e8082180cb391013d204ef9f...7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc)

---
updated-dependencies:
- dependency-name: shivammathur/setup-php
  dependency-version: 2.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 17:55:17 +01:00
nuno maduro e5ab3af05b ci: update dependabot config (add cooldown, single update entry) 2026-06-04 17:54:45 +01:00
nuno maduro 40b88b62ef release: v4.7.2 2026-06-01 07:08:59 +01:00
nuno maduro e3361bc321 fix: don't ob_start with pao 2026-06-01 07:08:50 +01:00
nuno maduro 92e76eb5ab ci: runs ci only against stable 2026-06-01 06:32:29 +01:00
nuno maduro bd22f478b8 chore: fixes issues with contracts and symfony 8.1 2026-06-01 06:24:42 +01:00
nuno maduro eeaac34cf6 release: v4.7.1 2026-06-01 05:42:12 +01:00
nuno maduro b9b07d8983 chore: bump dependencies 2026-06-01 05:42:03 +01:00
nuno maduro 6aa7d2f891 fix: better fatal exceptions reporting 2026-06-01 05:41:58 +01:00
nuno maduro 1c21a7647a chore: fixes types 2026-05-13 12:20:00 +01:00
nuno maduro d649de1988 chore: add security policy 2026-05-12 02:48:25 +01:00
nuno maduro 783ca4bcd6 chore(deps): limit dependabot to maintained branches (4.x + 5.x) 2026-05-12 02:34:08 +01:00
nuno maduro ba07497219 chore: enable Dependabot version updates for GitHub Actions (#1700) 2026-05-11 22:12:07 -03:00
nuno maduro 1ca021dea6 chore: pin GitHub Actions to commit SHAs (#1695)
* chore: pin GitHub Actions to commit SHAs

* chore: pin GitHub Actions to commit SHAs
2026-05-12 02:08:47 +01:00
nuno maduro 2fc75cfcf0 chore: updates snapshots 2026-05-03 13:09:32 -03:00
nuno maduro 6cc48f63f8 chore: style 2026-05-03 13:06:24 -03:00
nuno maduro e0419d1328 release: v4.7.0 2026-05-03 12:46:24 -03:00
nuno maduro faa6988801 Merge pull request #1682 from pestphp/feat/tia
[4.x] TIA Engine
2026-05-03 16:27:58 +01:00
nuno maduro c12247fafd Revert "wip"
This reverts commit 1b168aba1c.
2026-05-03 11:45:39 -03:00
nuno maduro 29b4452443 wip 2026-05-03 11:33:30 -03:00
nuno maduro 1b168aba1c wip 2026-05-03 11:30:13 -03:00
nuno maduro 6aabd977cd wip 2026-05-03 11:01:52 -03:00
nuno maduro a882543c53 wip 2026-05-03 10:57:28 -03:00
nuno maduro c250b9da4f wip 2026-05-03 10:37:17 -03:00
nuno maduro 46bc3dc628 wip 2026-05-03 10:34:44 -03:00
nuno maduro d3ce498b8a wip 2026-05-03 10:31:47 -03:00
nuno maduro e1a4b98b71 wip 2026-05-03 10:16:10 -03:00
nuno maduro 9afbcd5c18 wuip 2026-05-03 09:54:02 -03:00
nuno maduro 75593b6454 wip 2026-05-03 13:37:27 +01:00
nuno maduro 89590d6120 wip 2026-05-03 13:35:01 +01:00
nuno maduro fb0978c9bf wip 2026-05-03 13:26:48 +01:00
nuno maduro a3796daa42 wip 2026-05-03 13:24:10 +01:00
nuno maduro e3004db666 wip 2026-05-03 12:43:38 +01:00
nuno maduro fcf5c27914 chore: adds YouTube channel badge
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:56:04 -07:00
29 changed files with 372 additions and 201 deletions
+13
View 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.
+12
View File
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 5
groups:
github-actions:
patterns:
- "*"
+6 -4
View File
@@ -24,14 +24,14 @@ jobs:
strategy:
fail-fast: true
matrix:
dependency-version: [prefer-lowest, prefer-stable]
dependency-version: [prefer-stable]
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: 8.3
tools: composer:v2
@@ -44,7 +44,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: static-php-8.3-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
@@ -53,6 +53,8 @@ jobs:
static-php-8.3-composer-
- name: Install Dependencies
env:
COMPOSER_ROOT_VERSION: 4.x-dev
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
- name: Profanity Check
+5 -3
View File
@@ -35,10 +35,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
@@ -51,7 +51,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
@@ -66,6 +66,8 @@ jobs:
- name: Install PHP dependencies
shell: bash
env:
COMPOSER_ROOT_VERSION: 4.x-dev
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
- name: Unit Tests
+2 -1
View File
@@ -1,11 +1,12 @@
<p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v4/social.png" width="600" alt="PEST">
<p align="center">
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=4.x&label=Tests%204.x"></a>
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (4.x)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=4.x&label=Tests%204.x"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="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://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>
+5 -5
View File
@@ -26,12 +26,12 @@
"pestphp/pest-plugin-arch": "^4.0.2",
"pestphp/pest-plugin-mutate": "^4.0.1",
"pestphp/pest-plugin-profanity": "^4.2.1",
"phpunit/phpunit": "^12.5.24",
"symfony/process": "^7.4.8|^8.0.8"
"phpunit/phpunit": "^12.5.30",
"symfony/process": "^7.4.13|^8.1.0"
},
"conflict": {
"filp/whoops": "<2.18.3",
"phpunit/phpunit": ">12.5.24",
"phpunit/phpunit": ">12.5.30",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
@@ -59,11 +59,11 @@
]
},
"require-dev": {
"mrpunyapal/peststan": "^0.2.9",
"mrpunyapal/peststan": "^0.2.11",
"pestphp/pest-dev-tools": "^4.1.0",
"pestphp/pest-plugin-browser": "^4.3.1",
"pestphp/pest-plugin-type-coverage": "^4.0.4",
"psy/psysh": "^0.12.22"
"psy/psysh": "^0.12.24"
},
"minimum-stability": "dev",
"prefer-stable": true,
+4
View File
@@ -4,7 +4,9 @@ declare(strict_types=1);
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveUselessUnionReturnDocblockRector;
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
@@ -18,6 +20,8 @@ return RectorConfig::configure()
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class,
RemoveParentDelegatingConstructorRector::class,
RemoveDuplicatedReturnSelfDocblockRector::class,
RemoveUselessUnionReturnDocblockRector::class,
])
->withPreparedSets(
deadCode: true,
+5
View File
@@ -435,6 +435,11 @@ trait Testable
if ($hasOutputExpectation) {
ob_clean();
Closure::bind(function (): void {
$this->outputExpectedString = null;
$this->outputExpectedRegex = null;
}, $this, TestCase::class)();
}
$this->setUp();
+7 -4
View File
@@ -59,12 +59,15 @@ final class Project
/**
* Sets the test project to GitLab.
*/
public function gitlab(string $project): self
public function gitlab(string $project, string $hostname = 'gitlab.com'): self
{
$this->issues = "https://gitlab.com/{$project}/issues/%s";
$this->prs = "https://gitlab.com/{$project}/merge_requests/%s";
// Simple way to ensure only the host is used
$hostname = parse_url($hostname, PHP_URL_HOST) ?? $hostname;
$this->assignees = 'https://gitlab.com/%s';
$this->issues = "https://{$hostname}/{$project}/-/work_items/%s";
$this->prs = "https://{$hostname}/{$project}/-/merge_requests/%s";
$this->assignees = "https://{$hostname}/%s";
return $this;
}
+11 -5
View File
@@ -112,7 +112,7 @@ final class Expectation
if (function_exists('dump')) {
dump($this->value, ...$arguments);
} else {
var_dump($this->value);
var_dump($this->value, ...$arguments);
}
return $this;
@@ -120,16 +120,22 @@ final class Expectation
/**
* Dump the expectation value and end the script.
*
* @return never
*/
public function dd(mixed ...$arguments): void
public function dd(mixed ...$arguments): never
{
if (function_exists('dd')) {
dd($this->value, ...$arguments);
}
var_dump($this->value);
if (getenv('PARATEST') !== false || isset($_SERVER['COLLISION_PRINTER'])) {
ob_start();
var_dump($this->value, ...$arguments);
$output = ob_get_clean();
throw new ExpectationFailedException($output);
}
var_dump($this->value, ...$arguments);
exit(1);
}
+5 -2
View File
@@ -94,7 +94,8 @@ final class TestCaseFactory
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename);
}
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
$realpath = (string) realpath($filename);
$filename = str_replace('\\\\', '\\', addslashes($realpath));
$rootPath = TestSuite::getInstance()->rootPath;
$relativePath = str_replace($rootPath.DIRECTORY_SEPARATOR, '', $filename);
@@ -149,6 +150,8 @@ final class TestCaseFactory
$attributesCode = Attributes::code($this->attributes);
$filenameLiteral = var_export($realpath, true);
$methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(),
$methods
@@ -166,7 +169,7 @@ final class TestCaseFactory
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode
public static \$__filename = '$filename';
public static \$__filename = $filenameLiteral;
$methodsCode
}
+1 -1
View File
@@ -163,7 +163,7 @@ final class Kernel
$this->terminate();
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;
}
+10 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest;
use Laravel\Pao\Execution;
use Pest\Support\View;
use Symfony\Component\Console\Output\OutputInterface;
@@ -28,6 +29,10 @@ final class KernelDump
*/
public function enable(): void
{
if (class_exists(Execution::class) && Execution::running()) {
return;
}
ob_start(function (string $message): string {
$this->buffer .= $message;
@@ -68,6 +73,10 @@ final class KernelDump
$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)) {
$type = 'ERROR';
$this->buffer = str_replace(
@@ -107,7 +116,6 @@ final class KernelDump
*/
private function isInternalError(string $output): bool
{
return str_contains($output, 'An error occurred inside PHPUnit.')
|| str_contains($output, 'Fatal error');
return str_contains($output, 'An error occurred inside PHPUnit.');
}
}
+1
View File
@@ -954,6 +954,7 @@ final class Expectation
} catch (Throwable $e) {
if ($exception instanceof Throwable) {
// @phpstan-ignore-next-line
expect($e)
->toBeInstanceOf($exception::class, $message)
->and($e->getMessage())->toBe($exceptionMessage ?? $exception->getMessage(), $message);
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '4.6.3';
return '4.7.5';
}
function testDirectory(string $file = ''): string
@@ -146,7 +146,6 @@ final class WrapperRunner implements RunnerInterface
public function run(): int
{
$directory = dirname(__DIR__);
assert($directory !== '');
ExcludeList::addDirectory($directory);
TestResultFacade::init();
EventFacade::instance()->seal();
+81 -19
View File
@@ -10,6 +10,7 @@ use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Contracts\Plugins\Terminable;
use Pest\Exceptions\NoAffectedTestsFound;
use Pest\Panic;
use Pest\Plugins\Concerns\HandleArguments;
use Pest\Plugins\Tia\BaselineSync;
use Pest\Plugins\Tia\ChangedFiles;
use Pest\Plugins\Tia\Contracts\State;
@@ -36,7 +37,7 @@ use Symfony\Component\Process\Process;
*/
final class Tia implements AddsOutput, HandlesArguments, Terminable
{
use Concerns\HandleArguments;
use HandleArguments;
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 BASELINE_PATH_OPTION = '--baseline';
private const string ENV_TIA = 'PEST_TIA';
private const string ENV_FILTERED = 'PEST_TIA_FILTERED';
@@ -138,6 +141,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private bool $filteredMode = false;
private ?string $driftLabel = null;
private ?string $driftDetails = null;
private ?string $freshGraphReason = null;
public function __construct(
private readonly OutputInterface $output,
private readonly Recorder $recorder,
@@ -210,8 +219,11 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
if (! $watchPatterns->isEnabled()) {
return false;
}
if (! $watchPatterns->isLocally()) {
return true;
}
return ! ($watchPatterns->isLocally() && self::argumentPresent('--ci', $arguments));
return ! self::argumentPresent('--ci', $arguments);
}
/**
@@ -230,7 +242,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`.
*
* @param array<int, string> $arguments
@@ -242,7 +254,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return true;
}
if (str_starts_with($arg, "$argument=")) {
if (str_starts_with((string) $arg, "$argument=")) { // @phpstan-ignore-line
return true;
}
}
@@ -309,6 +321,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
*/
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();
$recordingGlobal = $isWorker && (string) Parallel::getGlobal(self::RECORDING_GLOBAL) === '1';
$replayingGlobal = $isWorker && (string) Parallel::getGlobal(self::REPLAYING_GLOBAL) === '1';
@@ -322,7 +340,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
&& (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL);
$enabled = ! $disabled && ($cliEnabled || $alwaysEnabled);
$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);
$this->forceRefetch = $this->hasArgument(self::REFETCH_OPTION, $arguments);
@@ -557,10 +576,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
if (! Fingerprint::structuralMatches($stored, $current)) {
$drift = Fingerprint::structuralDrift($stored, $current);
$this->renderChild(sprintf(
'Graph structure outdated (%s).',
$this->formatStructuralDrift($drift),
));
$this->driftLabel = $this->formatStructuralDrift($drift);
if (in_array('composer_lock', $drift, true)) {
$branchSha = $graph->recordedAtSha($this->branch);
@@ -570,7 +586,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$branchSha,
);
if ($summary !== '') {
$this->renderChild($summary);
$this->driftDetails = $summary;
}
}
}
@@ -654,6 +670,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
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);
}
@@ -919,7 +939,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
$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) === 1 ? '' : 's',
$reasons === [] ? '' : ' ('.implode(', ', $reasons).')',
@@ -980,7 +1000,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
$this->output->writeln('');
$this->renderChild('TIA mode enabled / fresh graph.');
$this->renderFreshGraph();
return $arguments;
}
@@ -989,7 +1009,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$this->recordingActive = true;
$this->output->writeln('');
$this->renderChild('TIA mode enabled / fresh graph.');
$this->renderFreshGraph();
return $arguments;
}
@@ -1002,11 +1022,32 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
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
{
$this->renderBadge('WARN', 'No coverage driver is available — skipped.');
$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->output->writeln('');
$this->renderChild('Running in TIA mode, however TIA as skipped as it needs Needs ext-pcov or Xdebug.');
}
/**
@@ -1324,7 +1365,16 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
/** @var ResultCollector $collector */
$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(
$this->branch,
$testId,
@@ -1332,10 +1382,12 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$result['message'],
$result['time'],
$result['assertions'],
$result['file'] ?? null,
$file,
);
}
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$collector->reset();
}
@@ -1358,6 +1410,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return;
}
$touchedFiles = [];
foreach ($results as $testId => $result) {
$file = $result['file'] ?? null;
@@ -1365,6 +1419,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$file = $this->resolveFailedTestFile($testId);
}
if (is_string($file) && $file !== '') {
$touchedFiles[$file] = true;
}
$graph->setResult(
$this->branch,
$testId,
@@ -1376,6 +1434,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
);
}
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$this->saveGraph($graph);
$collector->reset();
}
@@ -1432,6 +1492,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
foreach ($arguments as $index => $arg) {
$arg = (string) $arg; // @phpstan-ignore-line
if ($arg === '') {
continue;
}
@@ -1541,7 +1603,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
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;
}
+17 -32
View File
@@ -109,11 +109,6 @@ final readonly class BaselineSync
$this->clearCooldown();
$this->renderBadge('INFO', sprintf(
'Baseline ready (%s).',
$this->formatSize($payload['sizeOnDisk']),
));
return true;
}
@@ -244,7 +239,7 @@ final readonly class BaselineSync
if (is_file($runCacheDir.DIRECTORY_SEPARATOR.self::GRAPH_ASSET)) {
@touch($runCacheDir);
$this->renderBadge('INFO', sprintf(
$this->renderChild(sprintf(
'Using cached baseline from %s (run %s).',
$repo,
$runId,
@@ -312,14 +307,15 @@ final readonly class BaselineSync
{
$artifactSize = $this->artifactSize($repo, $runId);
$this->renderBadge('INFO', $artifactSize !== null
$this->output->writeln('');
$this->renderChild($artifactSize !== null
? sprintf(
'Fetching baseline (%s) from %s…',
'Downloading TIA baseline (%s) from %s…',
$this->formatSize($artifactSize),
$repo,
)
: sprintf(
'Fetching baseline from %s…',
'Downloading TIA baseline from %s…',
$repo,
));
@@ -333,10 +329,11 @@ final readonly class BaselineSync
$process->start();
$startedAt = microtime(true);
$tick = 0;
while ($process->isRunning()) {
$this->renderDownloadProgress($runCacheDir, $artifactSize, $startedAt);
usleep(250_000);
$this->renderDownloadProgress($startedAt, $tick++);
usleep(120_000);
}
$process->wait();
@@ -402,30 +399,18 @@ final readonly class BaselineSync
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);
$elapsed = max(0.001, microtime(true) - $startedAt);
$speed = (int) ($current / $elapsed);
static $frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
if ($totalBytes !== null && $totalBytes > 0) {
$percent = min(99, (int) floor(($current / $totalBytes) * 100));
$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),
);
}
$elapsed = max(0.0, microtime(true) - $startedAt);
$frame = $frames[$tick % count($frames)];
$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
+9
View File
@@ -31,6 +31,7 @@ final class CoverageMerger
$current = self::requireCoverage($reportPath);
if ($current instanceof CodeCoverage) {
self::primeUncoveredFiles($current);
$state->write(Tia::KEY_COVERAGE_CACHE, self::compress(serialize($current)));
}
@@ -52,6 +53,9 @@ final class CoverageMerger
return;
}
self::primeUncoveredFiles($cached);
self::primeUncoveredFiles($current);
self::stripCurrentTestsFromCached($cached, $current);
$cached->merge($current);
@@ -65,6 +69,11 @@ final class CoverageMerger
$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
{
$compressed = @gzencode($bytes);
+47 -101
View File
@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Pest\Plugins\Tia;
use Symfony\Component\Finder\Finder;
/**
* @internal
*/
final readonly class Fingerprint
{
private const int SCHEMA_VERSION = 15;
private const int SCHEMA_VERSION = 17;
/**
* @return array{
@@ -23,15 +25,15 @@ final readonly class Fingerprint
'structural' => [
'schema' => self::SCHEMA_VERSION,
'composer_lock' => self::composerLockHash($projectRoot),
'phpunit_xml' => self::hashIfExists($projectRoot.'/phpunit.xml'),
'phpunit_xml_dist' => self::hashIfExists($projectRoot.'/phpunit.xml.dist'),
'pest_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseFactory.php'),
'pest_method_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseMethodFactory.php'),
'phpunit_xml' => self::trackedHash($projectRoot, 'phpunit.xml'),
'phpunit_xml_dist' => self::trackedHash($projectRoot, 'phpunit.xml.dist'),
// 'pest_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseFactory.php'),
// 'pest_method_factory' => self::contentHashOrNull(__DIR__.'/../../Factories/TestCaseMethodFactory.php'),
'vite_config' => self::viteConfigHash($projectRoot),
'package_json' => self::packageJsonHash($projectRoot),
// 'package_json' => self::packageJsonHash($projectRoot),
'package_lock' => self::packageLockHash($projectRoot),
'js_config' => self::jsConfigHash($projectRoot),
'composer_json' => self::composerJsonHash($projectRoot),
// 'composer_json' => self::composerJsonHash($projectRoot),
],
'environmental' => [
'php_minor' => PHP_MAJOR_VERSION,
@@ -160,6 +162,10 @@ final readonly class Fingerprint
$parts = [];
foreach (JsModuleGraph::VITE_CONFIG_NAMES as $name) {
if (! self::isTrackedByGit($projectRoot, $name)) {
continue;
}
$hash = self::contentHashOrNull($projectRoot.'/'.$name);
if ($hash !== null) {
@@ -175,6 +181,10 @@ final readonly class Fingerprint
$parts = [];
foreach (['tsconfig.json', 'tsconfig.app.json', 'jsconfig.json'] as $name) {
if (! self::isTrackedByGit($projectRoot, $name)) {
continue;
}
$hash = self::hashIfExists($projectRoot.'/'.$name);
if ($hash !== null) {
@@ -185,52 +195,9 @@ final readonly class Fingerprint
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
{
return self::hashIfExists($projectRoot.'/composer.lock');
return self::trackedHash($projectRoot, 'composer.lock');
}
private static function packageLockHash(string $projectRoot): ?string
@@ -238,7 +205,7 @@ final readonly class Fingerprint
$parts = [];
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) {
$parts[] = $name.':'.$hash;
@@ -248,68 +215,47 @@ final readonly class Fingerprint
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 (! is_file($path)) {
if (! self::isTrackedByGit($projectRoot, $relativePath)) {
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;
}
$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);
return self::hashIfExists($projectRoot.'/'.$relativePath);
}
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)) {
return;
if (! is_file($projectRoot.'/'.$relativePath)) {
return false;
}
$isAssoc = ! array_is_list($value);
static $cache = [];
if ($isAssoc) {
ksort($value);
$key = $projectRoot."\0".$relativePath;
if (isset($cache[$key])) {
return $cache[$key];
}
foreach ($value as &$child) {
self::sortRecursively($child);
if (! is_dir($projectRoot.'/.git') && ! is_file($projectRoot.'/.git')) {
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
+45
View File
@@ -1321,6 +1321,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
{
$data = json_decode($json, true);
+3
View File
@@ -24,6 +24,9 @@ final class PcovRestarter implements Restarter
}
if (getenv(self::ENV_RESTARTED) === '1') {
putenv(self::ENV_RESTARTED);
unset($_ENV[self::ENV_RESTARTED]);
return;
}
+15 -4
View File
@@ -15,15 +15,20 @@ use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final readonly class EnsureTeamCityEnabled implements ConfiguredSubscriber
final class EnsureTeamCityEnabled implements ConfiguredSubscriber
{
/**
* Indicates if the TeamCity logger has already been registered.
*/
private static bool $registered = false;
/**
* Creates a new Configured Subscriber instance.
*/
public function __construct(
private InputInterface $input,
private OutputInterface $output,
private TestSuite $testSuite,
private readonly InputInterface $input,
private readonly OutputInterface $output,
private readonly TestSuite $testSuite,
) {}
/**
@@ -31,10 +36,16 @@ final readonly class EnsureTeamCityEnabled implements ConfiguredSubscriber
*/
public function notify(Configured $event): void
{
if (self::$registered) {
return;
}
if (! $this->input->hasParameterOption('--teamcity')) {
return;
}
self::$registered = true;
$flowId = getenv('FLOW_ID');
$flowId = is_string($flowId) ? (int) $flowId : getmypid();
@@ -1,5 +1,5 @@
Pest Testing Framework 4.6.3.
Pest Testing Framework 4.7.5.
USAGE: pest <file> [options]
@@ -1,3 +1,3 @@
Pest Testing Framework 4.6.3.
Pest Testing Framework 4.7.5.
+42 -2
View File
@@ -4,7 +4,6 @@
✓ preset → strict → ignoring ['Pest\Plugins\Tia\BaselineSync', 'usleep']
✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …]
✓ globals
✓ contracts
PASS Tests\Environments\Windows
✓ global functions are loaded
@@ -1716,6 +1715,47 @@
PASS Tests\Unit\Plugins\Retry
✓ 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\Plugins\Tia\IsEnabledForRun
✓ does not throw when an integer --random-order-seed is passed as a separate argv element
✓ still detects --tia when an integer argument is present
PASS Tests\Unit\Preset
✓ preset invalid name
✓ preset → myFramework
@@ -1901,4 +1941,4 @@
✓ pass with dataset with ('my-datas-set-value')
✓ within describe → pass with dataset with ('my-datas-set-value')
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1294 passed (2971 assertions)
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1330 passed (3010 assertions)
-10
View File
@@ -33,13 +33,3 @@ arch('globals')
->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep'])
->not->toBeUsed()
->ignoring(Expectation::class);
arch('contracts')
->expect('Pest\Contracts')
->toOnlyUse([
'NunoMaduro\Collision\Contracts',
'Pest\Factories\TestCaseMethodFactory',
'Symfony\Component\Console',
'Pest\Arch\Contracts',
'Pest\PendingCalls',
])->toBeInterfaces();
@@ -0,0 +1,20 @@
<?php
use Pest\Plugins\Tia;
test('does not throw when an integer --random-order-seed is passed as a separate argv element', function () {
// Mirrors the parallel worker argv (see bin/worker.php), where
// `--random-order-seed` and its value arrive as separate items and the
// seed value is an int rather than a string. Regression test for the
// str_starts_with() TypeError in Tia::argumentPresent() — the same class
// of bug as #1206, which was only fixed in HandleArguments.
$arguments = ['--order-by=random', '--random-order-seed', 1782350398];
expect(Tia::isEnabledForRun($arguments))->toBeFalse();
});
test('still detects --tia when an integer argument is present', function () {
$arguments = ['--tia', '--random-order-seed', 1782350398];
expect(Tia::isEnabledForRun($arguments))->toBeTrue();
});
+3 -2
View File
@@ -16,6 +16,7 @@ $run = function () {
test('parallel', function () use ($run) {
$output = $run('--exclude-group=integration');
$output = implode("\n", array_slice(explode("\n", $output), -10));
if (getenv('REBUILD_SNAPSHOTS')) {
preg_match('/Tests:\s+(.+\(\d+ assertions\))/', $output, $matches);
@@ -23,13 +24,13 @@ test('parallel', function () use ($run) {
$file = file_get_contents(__FILE__);
$file = preg_replace(
'/\$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 (2959 assertions)';",
$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 (2959 assertions)';
expect($output)
->toContain("Tests: {$expected}")