Compare commits

...

95 Commits

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 02:50:20 +01:00
nuno maduro 1ef680c75d fix: tia 2026-07-18 02:28:29 +01:00
nuno maduro 820fa08313 fix: package lock fingerprint 2026-07-18 01:10:01 +01:00
nuno maduro 9e79e491e2 Merge branch '4.x' into 5.x 2026-07-17 16:38:01 +01:00
nuno maduro 1b83b8fcda release: v4.7.6 2026-07-17 16:33:29 +01:00
nuno maduro ff4aba2ffa chore: updates snapshots 2026-07-17 14:32:33 +01:00
nuno maduro feaac1ab7e chore: fixes rector ignore 2026-07-17 14:32:27 +01:00
nuno maduro bbc6ada21e chore: bumps deps 2026-07-17 14:32:17 +01:00
Sonali dudhia 3927dbfcdf fix: report dataset provider errors as failing tests (#1749)
Errors raised while resolving a test's dataset (missing named dataset,
throwing dataset closure) previously crashed the whole run. Now the data
provider catches them, wraps them in a DatasetProviderError, and the test
method rethrows the original throwable so the affected test fails cleanly
with a non-zero exit code while other tests keep running.
2026-07-17 14:13:01 +01:00
nuno maduro b52bbb4cca chore: snapshots 2026-07-14 15:11:20 +01:00
nuno maduro 52819501eb chore: fixes tia when not running on root 2026-07-14 14:34:00 +01:00
nuno maduro 7b18287374 feat: evals configuration 2026-07-07 19:10:35 +01:00
nuno maduro 33e3bf2888 chore: moves to phpunit 13.2 2026-07-07 13:40:02 +01:00
nuno maduro 72ab506d7b Merge branch '4.x' into 5.x 2026-07-06 18:47:25 +01:00
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 b5fed82883 chore: style 2026-06-25 20:24:28 +01:00
nuno maduro 145e20db75 chore: merges 4.x 2026-06-25 20:20:20 +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
Sonali dudhia b49cd150c9 feat: add toBeEmail expectation (#1735)
Adds `toBeEmail()` backed by `filter_var(FILTER_VALIDATE_EMAIL)`.
Mirrors the existing `toBeUrl()` pattern — no new dependencies, pure PHP.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 15:51:35 +01:00
Norby Baruani 5cfb4133bf [5.x] Make time-based sharding namespace-agnostic and forward --test-directory (#1677)
* refactor(shard): extract parseListTestsOutput for testability

* test(shard): characterize parseListTestsOutput current behavior

* fix(shard): parse any PHP FQCN namespace from --list-tests

* fix(shard): forward --test-directory to list-tests subprocess

* chore: lint + snapshot fixups

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(shard): make extracted helpers private, test via reflection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* revert visual_snapshot_of_help_command_output

* revert visual_snapshot_of_help_command_output

* keep function removeParallelArguments

* strip --processes argument when building list-tests command

The removeParallelArguments method was not filtering --processes flags, causing the list-tests subprocess to fail when parallel execution was enabled. This prevented time-based sharding from working correctly with the --parallel option.

Now both --parallel/-p and --processes arguments are removed from the command used to enumerate tests, ensuring the subprocess runs successfully.

* test: re-add namespace-agnostic sharding tests

- 5.x merge kept describe()-style test file, dropped PR #1677 tests for parseListTestsOutput + buildListTestsCommand. Re-add them in matching style.
- Also fix removeParallelArguments test broken by merge: source array_values() + strips --processes, so expects ['bin/pest','tests/'].

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 13:09:30 +01:00
nuno maduro 3c8bae5f05 chore: bumps dependencies 2026-06-12 20:30:22 +01:00
nuno maduro b2998bc69e chore: updates snapshots 2026-06-12 20:30:16 +01:00
nuno maduro 3876093cd2 fix: missing array values 2026-06-12 20:22:47 +01:00
Moshe Brodsky 932f8bcc07 consistent sharding logic when no shards file (#1710) 2026-06-12 20:19:30 +01:00
oddvalue d393799d2a Optimize buildFilterArgument in Shard plugin for compact regex generation and add comprehensive tests (#1675) 2026-06-12 20:06:50 +01:00
nuno maduro 0d7814ca16 chore: update snapshots 2026-06-12 18:02:06 +01:00
flap152 8467c64c22 fix: popArgument drops duplicate arguments breaking --parallel --exclude-gropup= (#1674)
* fix: popArgument drops duplicate arguments breaking --parallel multi-exclude-group

Fixes #1437

* fix: ensure popArgument handles duplicate arguments

* fix: update expected test results and snapshots after rebase

---------

Signed-off-by: nuno maduro <enunomaduro@gmail.com>
Co-authored-by: nuno maduro <enunomaduro@gmail.com>
2026-06-12 17:58:37 +01:00
oddvalue 97714a7088 Enforce filter length validation and add tests for Shard plugin (#1673) 2026-06-12 17:56:32 +01:00
nuno maduro afb582616d chore: points to 5.x 2026-06-12 07:29:53 +01:00
nuno maduro 15e9b6a507 chore: snapshots 2026-06-12 07:22:13 +01:00
nuno maduro 520ce29376 Merge branch '4.x' into 5.x 2026-06-12 07:22:01 +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
Sonali dudhia 774a340400 feat: add toBeUlid assertion and isUlid validation method (#1726) 2026-06-11 10:14:00 +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
dependabot[bot] 3d5bba93f8 Bump shivammathur/setup-php in the github-actions group (#1706)
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:13 +01:00
nuno maduro e5ab3af05b ci: update dependabot config (add cooldown, single update entry) 2026-06-04 17:54:45 +01:00
nuno maduro 79bc7a8257 Merge branch '4.x' into 5.x 2026-06-01 07:09:57 +01:00
nuno maduro fc48c1bd1e Merge branch '4.x' into 5.x 2026-06-01 06:33:35 +01:00
nuno maduro da726beffc chore: merges 4.x 2026-06-01 06:28:44 +01:00
nuno maduro 4ef12b9aac Merge branch '4.x' into 5.x 2026-06-01 06:25:56 +01:00
nuno maduro 4d550cecfd Merge branch '4.x' into 5.x 2026-05-13 12:20:46 +01:00
nuno maduro 34695843b3 chore: pin GitHub Actions to commit SHAs (#1699)
* chore: pin GitHub Actions to commit SHAs

* chore: pin GitHub Actions to commit SHAs
2026-05-11 22:12:04 -03:00
nuno maduro d17be9decd types 2026-05-04 08:02:09 -03:00
nuno maduro b828ddcec7 chore: style 2026-05-04 07:38:50 -03:00
nuno maduro f859bb179d Merge branch '4.x' into 5.x 2026-05-04 07:38:40 -03:00
nuno maduro 18bbca748f Merge branch '4.x' into 5.x 2026-04-18 07:03:46 -07:00
nuno maduro f142aad8ad Merge branch '4.x' into 5.x 2026-04-17 19:35:53 -07:00
nuno maduro 74a28d4f5e fix: wrapper runner 2026-04-17 07:29:03 -07:00
nuno maduro 6053e15d00 Merge branch '4.x' into 5.x 2026-04-17 06:07:14 -07:00
nuno maduro 2d649d765f chore: adjusts tests 2026-04-11 01:54:13 +01:00
nuno maduro 4fb4908570 Merge branch '4.x' into 5.x 2026-04-10 22:37:24 +01:00
nuno maduro e63a886f98 Merge pull request #1661 from Avnsh1111/fix/opposite-expectation-truncated-message
fix: preserve full error message in not() expectation failures
2026-04-10 11:48:24 +01:00
nuno maduro 8dd650fd05 Merge branch '4.x' into 5.x 2026-04-09 21:39:15 +01:00
nuno maduro fbca346d7c fix: types 2026-04-07 14:40:55 +01:00
nuno maduro 3f13bca0f7 just in case 2026-04-07 14:37:13 +01:00
nuno maduro d3acb1c56a fix: coverage 2026-04-07 14:33:41 +01:00
avinash e601e6df31 fix: preserve full error message in not() expectation failures
When using not() expectations with custom error messages, the message
was truncated because throwExpectationFailedException() passed all
arguments through shortenedExport() which limits strings to ~40 chars.

Uses the full export() method for arguments instead of shortenedExport()
so custom error messages are displayed in their entirety.

Fixes #1533
2026-04-07 18:12:54 +05:30
nuno maduro 6fdbca1226 fix: parallel testing 2026-04-06 23:37:49 +01:00
nuno maduro 54359b895f Merge branch '4.x' into 5.x 2026-04-06 21:57:41 +01:00
nuno maduro 44c04bfce1 chore: bumps paratest 2026-04-06 14:41:38 +01:00
nuno maduro 271c680d3c Merge branch '4.x' into 5.x 2026-04-06 11:24:05 +01:00
nuno maduro 4a1d8d27b8 chore: bumps dependencies 2026-04-03 12:12:27 +01:00
nuno maduro 0f6924984c Merge branch '4.x' into 5.x 2026-04-03 12:02:36 +01:00
nuno maduro 668ca9f5de feat: adds pao 2026-04-02 15:45:13 +01:00
nuno maduro f659a45311 Merge branch '4.x' into 5.x 2026-03-21 13:20:25 +00:00
nuno maduro 12c1da29ee Merge branch '4.x' into 5.x 2026-03-10 21:21:24 +00:00
Nuno Maduro fa27c8daef chore: version 2026-02-17 17:52:40 +00:00
Nuno Maduro f0a08f0503 chore: missing types 2026-02-17 17:52:00 +00:00
Nuno Maduro 2c040c5b1f chore: style 2026-02-17 17:45:50 +00:00
Nuno Maduro a9ce1fd739 chore: code refactor 2026-02-17 17:45:34 +00:00
Nuno Maduro 3533356262 chore: updates snapshots 2026-02-17 17:44:56 +00:00
Nuno Maduro 4aa41d0b14 chore: bumps dependencies 2026-02-17 17:41:38 +00:00
Nuno Maduro e4ed60085c chore: bumps dependencies 2026-02-17 17:18:45 +00:00
Nuno Maduro e2b119655d chore: point pestphp dependencies to ^5.0.0 2026-02-17 17:13:36 +00:00
Nuno Maduro fcf5baf0a9 chore: start preparing for pest 5.x 2026-02-17 16:55:03 +00:00
294 changed files with 4979 additions and 2364 deletions
+2 -9
View File
@@ -4,15 +4,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
target-branch: "5.x"
cooldown:
default-days: 5
groups:
github-actions:
patterns:
+10 -8
View File
@@ -2,7 +2,7 @@ name: Static Analysis
on:
push:
branches: [4.x]
branches: [5.x]
pull_request:
schedule:
- cron: '0 9 * * *'
@@ -28,12 +28,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: 8.3
php-version: 8.4
tools: composer:v2
coverage: none
extensions: sockets
@@ -44,15 +44,17 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # 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') }}
key: static-php-8.4-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
restore-keys: |
static-php-8.3-${{ matrix.dependency-version }}-composer-
static-php-8.3-composer-
static-php-8.4-${{ matrix.dependency-version }}-composer-
static-php-8.4-composer-
- name: Install Dependencies
env:
COMPOSER_ROOT_VERSION: 5.x-dev
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
- name: Profanity Check
+8 -9
View File
@@ -2,7 +2,7 @@ name: Tests
on:
push:
branches: [4.x]
branches: [5.x]
pull_request:
schedule:
- cron: '0 9 * * *'
@@ -24,21 +24,18 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest] # windows-latest
symfony: ['7.4', '8.0']
php: ['8.3', '8.4', '8.5']
symfony: ['8.0']
php: ['8.4', '8.5']
dependency_version: [prefer-stable]
exclude:
- php: '8.3'
symfony: '8.0'
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
@@ -51,7 +48,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # 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 +63,8 @@ jobs:
- name: Install PHP dependencies
shell: bash
env:
COMPOSER_ROOT_VERSION: 5.x-dev
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
- name: Unit Tests
+3 -3
View File
@@ -1,7 +1,7 @@
<p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v4/social.png" width="600" alt="PEST">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v5/social.png" width="600" alt="PEST">
<p align="center">
<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 (5.x)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=5.x&label=Tests%205.x"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="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>
@@ -12,7 +12,7 @@
------
> Pest v4 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing)**.
> Pest v5 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest-v5-is-here)**.
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP.
+3 -3
View File
@@ -2,10 +2,10 @@
When releasing a new version of Pest there are some checks and updates that need to be done:
> **For Pest v3 you should use the `3.x` branch instead.**
> **For Pest v4 you should use the `4.x` branch instead.**
- Clear your local repository with: `git add . && git reset --hard && git checkout 4.x`
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...4.x](https://github.com/pestphp/pest/compare/{latest_version}...4.x)
- Clear your local repository with: `git add . && git reset --hard && git checkout 5.x`
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...5.x](https://github.com/pestphp/pest/compare/{latest_version}...5.x)
- Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test`
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
+149 -14
View File
@@ -25,26 +25,72 @@ const PAGE_DIR_CANDIDATES = [
async function loadRolldown() {
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
const path = projectRequire.resolve('rolldown')
let path = null
try { path = projectRequire.resolve('rolldown') } catch {}
if (path === null) {
// rolldown-vite installs (vite@npm:rolldown-vite) ship rolldown as a
// dependency of the vite package rather than a top-level install.
try {
const viteRequire = createRequire(projectRequire.resolve('vite/package.json'))
path = viteRequire.resolve('rolldown')
} catch {}
}
if (path === null) return null
return await import(pathToFileURL(path).href)
}
export function stripJsonComments(raw) {
let out = ''
let inString = false
let quote = ''
let inLine = false
let inBlock = false
for (let i = 0; i < raw.length; i++) {
const c = raw[i]
const n = raw[i + 1]
if (inLine) {
if (c === '\n') { inLine = false; out += c }
continue
}
if (inBlock) {
if (c === '*' && n === '/') { inBlock = false; i++ }
continue
}
if (inString) {
out += c
if (c === '\\') { out += n ?? ''; i++; continue }
if (c === quote) inString = false
continue
}
if (c === '"' || c === "'") { inString = true; quote = c; out += c; continue }
if (c === '/' && n === '/') { inLine = true; i++; continue }
if (c === '/' && n === '*') { inBlock = true; i++; continue }
if (c === '}' || c === ']') out = out.replace(/,\s*$/, '')
out += c
}
return out
}
async function readJsonWithComments(path) {
const raw = await readFile(path, 'utf8')
const stripped = raw
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/[^\n]*/g, '$1')
return JSON.parse(stripped)
return JSON.parse(stripJsonComments(raw))
}
async function loadAliasFromTsconfig() {
export async function loadAliasFromTsconfig(projectRoot = PROJECT_ROOT) {
const alias = {}
for (const name of ['tsconfig.json', 'jsconfig.json']) {
const p = join(PROJECT_ROOT, name)
const p = join(projectRoot, name)
if (!existsSync(p)) continue
let cfg
try { cfg = await readJsonWithComments(p) } catch { continue }
const baseUrl = resolve(PROJECT_ROOT, cfg?.compilerOptions?.baseUrl ?? '.')
const baseUrl = resolve(projectRoot, cfg?.compilerOptions?.baseUrl ?? '.')
const paths = cfg?.compilerOptions?.paths ?? {}
for (const [key, targets] of Object.entries(paths)) {
if (!key.endsWith('/*')) continue
@@ -58,6 +104,74 @@ async function loadAliasFromTsconfig() {
return alias
}
const VITE_CONFIG_FILES = [
'vite.config.ts',
'vite.config.js',
'vite.config.mjs',
'vite.config.cjs',
'vite.config.mts',
'vite.config.cts',
]
function resolveAliasTarget(projectRoot, target) {
if (target.startsWith('/')) {
// Vite resolves a leading slash against the project root, unless the
// config author used a genuinely absolute path.
return existsSync(target) ? target : resolve(projectRoot, '.' + target)
}
return resolve(projectRoot, target)
}
async function usesLaravelVitePlugin(projectRoot) {
const p = join(projectRoot, 'package.json')
if (!existsSync(p)) return false
try {
const pkg = JSON.parse(await readFile(p, 'utf8'))
return Boolean(pkg.dependencies?.['laravel-vite-plugin'] ?? pkg.devDependencies?.['laravel-vite-plugin'])
} catch {
return false
}
}
export async function loadAliasFromViteConfig(projectRoot = PROJECT_ROOT) {
const alias = {}
let source = null
for (const name of VITE_CONFIG_FILES) {
const p = join(projectRoot, name)
if (!existsSync(p)) continue
try { source = await readFile(p, 'utf8') } catch { continue }
break
}
if (source !== null) {
// The config is executable code we cannot import safely, so extract alias
// entries textually: a quoted `@…`/`~…` key, then the last quoted path in
// the value expression — covers `'@': '/resources/js'` as well as
// `'@': path.resolve(__dirname, 'resources/js')`. The value stops at a
// top-level comma; call arguments are kept via the paren group.
for (const m of source.matchAll(/['"`]([@~][\w./-]*)['"`]\s*:\s*((?:\([^)\n]*\)|[^,\n])*)/g)) {
const key = m[1]
if (alias[key] !== undefined) continue
const paths = [...m[2].matchAll(/['"`]([^'"`]+)['"`]/g)].map((p) => p[1])
const target = paths.length > 0 ? paths[paths.length - 1] : null
if (!target || target.includes('*')) continue
alias[key] = resolveAliasTarget(projectRoot, target)
}
}
// laravel-vite-plugin registers '@' → resources/js by default.
if (alias['@'] === undefined && (await usesLaravelVitePlugin(projectRoot))) {
alias['@'] = resolve(projectRoot, 'resources/js')
}
return alias
}
async function listPageFiles(pagesDir) {
if (!existsSync(pagesDir)) return []
@@ -121,8 +235,16 @@ async function main() {
return
}
const { rolldown } = await loadRolldown()
const alias = await loadAliasFromTsconfig()
const loaded = await loadRolldown()
if (loaded === null) {
process.stdout.write('{}')
return
}
const { rolldown } = loaded
// The vite config is what the dev server actually resolves with — let it win.
const alias = { ...(await loadAliasFromTsconfig()), ...(await loadAliasFromViteConfig()) }
const aliasKeys = Object.keys(alias)
const graph = new Map()
@@ -167,7 +289,7 @@ async function main() {
cwd: PROJECT_ROOT,
resolve: {
alias,
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json'],
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.mjs', '.cjs', '.json', '.vue', '.svelte'],
},
transform: { jsx: 'preserve' },
treeshake: false,
@@ -192,6 +314,11 @@ async function main() {
stack.add(id)
const acc = new Set()
// A set computed while skipping an in-stack (cyclic) dependency is missing
// that dependency's subtree and must not be memoized — the ancestor call
// completes it for the current traversal, but a cached copy would leak the
// incomplete set into other pages' traversals.
let complete = true
const deps = graph.get(id)
if (deps) {
for (const dep of deps) {
@@ -200,13 +327,17 @@ async function main() {
const rel = relative(PROJECT_ROOT, dep).split(sep).join('/')
acc.add(rel)
}
if (stack.has(dep)) continue
if (stack.has(dep)) {
complete = false
continue
}
const child = computeTransitive(dep, stack)
if (child) for (const r of child) acc.add(r)
if (!transitiveCache.has(dep)) complete = false
}
}
stack.delete(id)
transitiveCache.set(id, acc)
if (complete) transitiveCache.set(id, acc)
return acc
}
@@ -230,10 +361,14 @@ async function main() {
process.stdout.write(JSON.stringify(payload))
}
const invokedDirectly = process.argv[1] !== undefined
&& import.meta.url === pathToFileURL(process.argv[1]).href
if (invokedDirectly) {
try {
void pathToFileURL
await main()
} catch (err) {
process.stderr.write(String(err?.stack ?? err ?? 'unknown error'))
process.exit(1)
}
}
+17 -16
View File
@@ -17,21 +17,20 @@
}
],
"require": {
"php": "^8.3.0",
"brianium/paratest": "^7.20.0",
"composer/xdebug-handler": "^3.0.5",
"nunomaduro/collision": "^8.9.4",
"php": "^8.4",
"brianium/paratest": "^7.23.0",
"nunomaduro/collision": "^8.9.5",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^4.0.0",
"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.28",
"symfony/process": "^7.4.13|^8.1.0"
"pestphp/pest-plugin": "^5.0.0",
"pestphp/pest-plugin-arch": "^5.0.0",
"pestphp/pest-plugin-mutate": "^5.0.0",
"pestphp/pest-plugin-profanity": "^5.0.0",
"phpunit/phpunit": "^13.2.4",
"symfony/process": "^8.1.0"
},
"conflict": {
"filp/whoops": "<2.18.3",
"phpunit/phpunit": ">12.5.28",
"phpunit/phpunit": ">13.2.4",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
@@ -59,11 +58,13 @@
]
},
"require-dev": {
"mrpunyapal/peststan": "^0.2.10",
"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.23"
"laravel/pao": "^1.1.2",
"pestphp/pest-dev-tools": "^5.0.0",
"pestphp/pest-plugin-browser": "^5.0.0",
"pestphp/pest-plugin-phpstan": "^5.0.0",
"pestphp/pest-plugin-rector": "^5.0.0",
"pestphp/pest-plugin-type-coverage": "^5.0.0",
"psy/psysh": "^0.12.24"
},
"minimum-stability": "dev",
"prefer-stable": true,
+1 -1
View File
@@ -1,7 +1,7 @@
includes:
- phpstan-baseline.neon
- phpstan-pest-extension.neon
- vendor/mrpunyapal/peststan/extension.neon
- vendor/pestphp/pest-plugin-phpstan/extension.neon
parameters:
level: 7
+39
View File
@@ -2,22 +2,61 @@
declare(strict_types=1);
use Pest\Rector\Rules\UseToMatchArrayRector;
use Pest\Rector\Set\PestSetList;
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveReturnTagIncompatibleWithNativeTypeRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveUselessUnionReturnDocblockRector;
use Rector\DeadCode\Rector\Property\RemoveUnusedPrivatePropertyRector;
use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return RectorConfig::configure()
->withPaths([
__DIR__.'/src',
__DIR__.'/tests',
])
->withSets([
PestSetList::PEST_CODE_QUALITY,
PestSetList::PEST_CHAIN,
])
->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
__DIR__.'/tests/Fixtures/Arch',
ReturnNeverTypeRector::class,
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class,
RemoveParentDelegatingConstructorRector::class,
RemoveDuplicatedReturnSelfDocblockRector::class,
RemoveUselessUnionReturnDocblockRector::class,
RemoveReturnTagIncompatibleWithNativeTypeRector::class => [
__DIR__.'/src/Expectations/HigherOrderExpectation.php',
],
// Merges unrelated expectations into a single `toMatchArray()`, turning
// `toContain()` into exact matches, dropping `->not`, and mistaking a
// `toBeTrue()` failure message for an expected value. Unsafe here.
UseToMatchArrayRector::class,
// Test fixtures rely on "unused" constructors, params and properties
// (resolved via the container or read through reflection), so the
// dead-code and return-type rules below must not touch the test suite.
RemoveEmptyClassMethodRector::class => [
__DIR__.'/tests',
],
RemoveUnusedConstructorParamRector::class => [
__DIR__.'/tests',
],
RemoveUnusedPrivatePropertyRector::class => [
__DIR__.'/tests',
],
AddArrowFunctionReturnTypeRector::class => [
__DIR__.'/tests',
],
])
->withPreparedSets(
deadCode: true,
@@ -7,7 +7,7 @@
<div>
<span class="text-gray mr-1">- </span>
<span>composer require pestphp/pest-plugin-browser:^4.0 --dev</span>
<span>composer require pestphp/pest-plugin-browser:^5.0 --dev</span>
</div>
<div>
+20 -5
View File
@@ -23,6 +23,8 @@ use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\IncompleteTest;
use PHPUnit\Framework\SkippedTest;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestCase\ExceptionExpectation;
use PHPUnit\Framework\TestCase\OutputBuffer;
use ReflectionException;
use ReflectionFunction;
use ReflectionParameter;
@@ -431,10 +433,15 @@ trait Testable
unset($this->{$property});
}
$hasOutputExpectation = Closure::bind(fn (): bool => is_string($this->outputExpectedString) || is_string($this->outputExpectedRegex), $this, TestCase::class)();
$outputBuffer = Closure::bind(fn () => $this->outputBuffer, $this, TestCase::class)();
if ($hasOutputExpectation) {
if ($outputBuffer->hasExpectation()) {
ob_clean();
Closure::bind(function (): void {
$this->expectedString = null;
$this->expectedRegularExpression = null;
}, $outputBuffer, OutputBuffer::class)();
}
$this->setUp();
@@ -450,7 +457,9 @@ trait Testable
*/
private function __isExpectedException(Throwable $e): bool
{
$read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $this, TestCase::class)();
$expectation = Closure::bind(fn () => $this->exceptionExpectation, $this, TestCase::class)();
$read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $expectation, ExceptionExpectation::class)();
$expectedClass = $read('expectedException');
@@ -458,13 +467,19 @@ trait Testable
return $e instanceof $expectedClass;
}
$expectedMessage = $read('expectedExceptionMessage');
$expectedMessage = $read('expectedMessage');
if ($expectedMessage !== null) {
return str_contains($e->getMessage(), (string) $expectedMessage);
}
$expectedCode = $read('expectedExceptionCode');
$expectedMessageRegex = $read('expectedMessageRegularExpression');
if ($expectedMessageRegex !== null) {
return preg_match($expectedMessageRegex, $e->getMessage()) === 1;
}
$expectedCode = $read('expectedCode');
if ($expectedCode !== null) {
return $e->getCode() === $expectedCode;
+11 -3
View File
@@ -33,7 +33,7 @@ final readonly class Configuration
*/
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
{
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
{
(new BeforeEachCall(TestSuite::getInstance(), $this->filename))->only();
new BeforeEachCall(TestSuite::getInstance(), $this->filename)->only();
}
/**
@@ -119,6 +119,14 @@ final readonly class Configuration
return new Browser\Configuration;
}
/**
* Gets the evals configuration.
*/
public function evals(): Evals\Configuration // @phpstan-ignore-line
{
return new Evals\Configuration; // @phpstan-ignore-line
}
/**
* Gets the TIA (Test Impact Analysis) configuration.
*/
+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;
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use RuntimeException;
use Throwable;
/**
* @internal
*/
final class DatasetProviderError extends RuntimeException
{
public function __construct(Throwable $previous)
{
parent::__construct($previous->getMessage(), (int) $previous->getCode(), $previous);
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TiaRequiresRepositoryRoot extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct(private readonly string $subdirectoryPrefix)
{
parent::__construct(sprintf(
'Tia mode requires the project root to be the git repository root, but it sits in the subdirectory [%s] of a larger repo.',
$this->subdirectoryPrefix,
));
}
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=red> ERROR </> Tia mode requires the git repository root.',
'',
sprintf(' This project sits in a subdirectory of a larger repo <fg=yellow>%s</>.', $this->subdirectoryPrefix),
'',
' Give the project its own git repository to use Tia.',
'',
]);
}
public function exitCode(): int
{
return 1;
}
}
+15 -17
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);
}
@@ -238,7 +244,7 @@ final class Expectation
if ($callbacks[$index] instanceof Closure) {
$callbacks[$index](new self($value), new self($key));
} else {
(new self($value))->toEqual($callbacks[$index]);
new self($value)->toEqual($callbacks[$index]);
}
$index = isset($callbacks[$index + 1]) ? $index + 1 : 0;
@@ -915,15 +921,7 @@ final class Expectation
return Targeted::make(
$this,
function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if (! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)),
"to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
@@ -1138,8 +1136,8 @@ final class Expectation
$this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass)
&& $object->reflectionClass->isEnum()
&& (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
&& (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line
&& new ReflectionEnum($object->name)->isBacked() // @phpstan-ignore-line
&& (string) new ReflectionEnum($object->name)->getBackingType() === $backingType, // @phpstan-ignore-line
'to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
+2 -2
View File
@@ -112,7 +112,7 @@ final class HigherOrderExpectation
* Dynamically calls methods on the class with the given arguments.
*
* @param array<int, mixed> $arguments
* @return self<TOriginalValue, mixed>|self<TOriginalValue, TValue>
* @return self<TOriginalValue, mixed>
*/
public function __call(string $name, array $arguments): self
{
@@ -127,7 +127,7 @@ final class HigherOrderExpectation
/**
* Accesses properties in the value or in the expectation.
*
* @return self<TOriginalValue, mixed>|self<TOriginalValue, TValue>
* @return self<TOriginalValue, mixed>
*/
public function __get(string $name): self
{
+6 -24
View File
@@ -576,15 +576,7 @@ final readonly class OppositeExpectation
return Targeted::make(
$original,
function (ObjectDescription $object) use ($traits): bool {
foreach ($traits as $trait) {
if (isset($object->reflectionClass) && in_array($trait, $object->reflectionClass->getTraitNames(), true)) {
return false;
}
}
return true;
},
fn (ObjectDescription $object): bool => array_all($traits, fn (string $trait): bool => ! isset($object->reflectionClass) || ! in_array($trait, $object->reflectionClass->getTraitNames(), true)),
"not to use traits '".implode("', '", $traits)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
@@ -604,15 +596,7 @@ final readonly class OppositeExpectation
return Targeted::make(
$original,
function (ObjectDescription $object) use ($interfaces): bool {
foreach ($interfaces as $interface) {
if (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)) {
return false;
}
}
return true;
},
fn (ObjectDescription $object): bool => array_all($interfaces, fn (string $interface): bool => ! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)),
"not to implement '".implode("', '", $interfaces)."'",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
@@ -814,13 +798,11 @@ final readonly class OppositeExpectation
$exporter = Exporter::default();
$toString = fn (mixed $argument): string => $exporter->shortenedExport($argument);
throw new ExpectationFailedException(sprintf(
'Expecting %s not %s %s.',
$toString($this->original->value),
$exporter->shortenedExport($this->original->value),
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,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false
|| ! $object->reflectionClass->isEnum()
|| ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line
|| (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line
|| ! new \ReflectionEnum($object->name)->isBacked() // @phpstan-ignore-line
|| (string) new \ReflectionEnum($object->name)->getBackingType() !== $backingType, // @phpstan-ignore-line
'not to be '.$backingType.' backed enum',
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
);
+7 -3
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
@@ -158,6 +161,7 @@ final class TestCaseFactory
$classCode = <<<PHP
namespace $namespace;
use Pest\Exceptions\DatasetProviderError as __PestDatasetProviderError;
use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
@@ -166,7 +170,7 @@ final class TestCaseFactory
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode
public static \$__filename = '$filename';
public static \$__filename = $filenameLiteral;
$methodsCode
}
@@ -197,7 +201,7 @@ final class TestCaseFactory
if (
$method->closure instanceof \Closure &&
(new \ReflectionFunction($method->closure))->isStatic()
new \ReflectionFunction($method->closure)->isStatic()
) {
throw new TestClosureMustNotBeStatic($method);
+8
View File
@@ -235,6 +235,10 @@ final class TestCaseMethodFactory
$attributesCode
public function $methodName(...\$arguments)
{
if (count(\$arguments) === 1 && \$arguments[0] instanceof __PestDatasetProviderError) {
throw \$arguments[0]->getPrevious() ?? \$arguments[0];
}
return \$this->__runTest(
\$this->__test,
...\$arguments,
@@ -261,7 +265,11 @@ final class TestCaseMethodFactory
public static function $dataProviderName()
{
try {
return __PestDatasets::get(self::\$__filename, "$methodName");
} catch (\Throwable \$throwable) {
return [[new __PestDatasetProviderError(\$throwable)]];
}
}
EOF;
+6
View File
@@ -241,6 +241,12 @@ final class TeamCityLogger
$telemetry->memoryUsageSinceStart(),
$telemetry->durationSincePrevious(),
$telemetry->memoryUsageSincePrevious(),
$telemetry->userCpuTimeSinceStart(),
$telemetry->systemCpuTimeSinceStart(),
$telemetry->totalCpuTimeSinceStart(),
$telemetry->userCpuTimeSincePrevious(),
$telemetry->systemCpuTimeSincePrevious(),
$telemetry->totalCpuTimeSincePrevious(),
);
}
+33 -1
View File
@@ -936,7 +936,7 @@ final class Expectation
if ($exception instanceof Closure) {
$callback = $exception;
$parameters = (new ReflectionFunction($exception))->getParameters();
$parameters = new ReflectionFunction($exception)->getParameters();
if (count($parameters) !== 1) {
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
@@ -1142,6 +1142,22 @@ final class Expectation
return $this;
}
/**
* Asserts that the value is a ULID.
*
* @return self<TValue>
*/
public function toBeUlid(string $message = ''): self
{
if (! is_string($this->value)) {
InvalidExpectationValue::expected('string');
}
Assert::assertTrue(Str::isUlid($this->value), $message);
return $this;
}
/**
* Asserts that the value is between 2 specified values
*
@@ -1155,6 +1171,22 @@ final class Expectation
return $this;
}
/**
* Asserts that the value is an email address.
*
* @return self<TValue>
*/
public function toBeEmail(string $message = ''): self
{
if ($message === '') {
$message = "Failed asserting that {$this->value} is an email address.";
}
Assert::assertTrue(Str::isEmail((string) $this->value), $message);
return $this;
}
/**
* Asserts that the value is a url
*
@@ -19,7 +19,7 @@ use PHPStan\Type\Type;
* $expectation, $opposite, $shouldReset) from being incorrectly resolved as
* higher-order value property accesses by downstream ExpressionTypeResolverExtensions.
*
* This extension must be registered BEFORE the peststan HigherOrderExpectationTypeExtension.
* This extension must be registered BEFORE the pest-plugin-phpstan HigherOrderExpectationTypeExtension.
*
* @internal
*/
@@ -37,7 +37,7 @@ final readonly class HigherOrderExpectationTypeExtension implements ExpressionTy
$varType = $scope->getType($expr->var);
if (! (new ObjectType(HigherOrderExpectation::class))->isSuperTypeOf($varType)->yes()) {
if (! new ObjectType(HigherOrderExpectation::class)->isSuperTypeOf($varType)->yes()) {
return null;
}
+1 -3
View File
@@ -53,9 +53,7 @@ final class UsesCall
$this->targets = [$filename];
}
/**
* @deprecated Use `pest()->printer()->compact()` instead.
*/
#[\Deprecated(message: 'Use `pest()->printer()->compact()` instead.')]
public function compact(): self
{
DefaultPrinter::compact(true);
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '4.7.2';
return '5.0.0-beta.1';
}
function testDirectory(string $file = ''): string
+1 -1
View File
@@ -50,7 +50,7 @@ final class Cache implements HandlesArguments
}
}
if (! $this->hasArgument('--parallel', $arguments)) {
if (! $this->hasArgument('--parallel', $arguments) && ! $this->hasArgument('--do-not-cache-result', $arguments) && ! $this->hasArgument('--cache-result', $arguments)) {
return $this->pushArgument('--cache-result', $arguments);
}
+6 -3
View File
@@ -50,11 +50,14 @@ trait HandleArguments
*/
public function popArgument(string $argument, array $arguments): array
{
$arguments = array_flip($arguments);
$key = array_search($argument, $arguments, true);
unset($arguments[$argument]);
while ($key !== false) {
unset($arguments[$key]);
$key = array_search($argument, $arguments, true);
}
return array_values(array_flip($arguments));
return array_values($arguments);
}
/**
+3 -3
View File
@@ -17,6 +17,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
final class Coverage implements AddsOutput, HandlesArguments
{
use Concerns\HandleArguments;
private const string COVERAGE_OPTION = 'coverage';
private const string MIN_OPTION = 'min';
@@ -77,11 +79,9 @@ final class Coverage implements AddsOutput, HandlesArguments
return false;
}))];
$originals = array_flip($originals);
foreach ($arguments as $argument) {
unset($originals[$argument]);
$originals = $this->popArgument($argument, $originals);
}
$originals = array_flip($originals);
$inputs = [];
$inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE);
+1 -7
View File
@@ -178,13 +178,7 @@ final class Parallel implements HandlesArguments
{
$arguments = new ArgvInput;
foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) {
if ($arguments->hasParameterOption($unsupportedArgument)) {
return true;
}
}
return false;
return array_any(self::UNSUPPORTED_ARGUMENTS, fn (string|array $unsupportedArgument): bool => $arguments->hasParameterOption($unsupportedArgument));
}
/**
@@ -7,7 +7,6 @@ namespace Pest\Plugins\Parallel\Paratest;
use const DIRECTORY_SEPARATOR;
use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection;
use ParaTest\Coverage\CoverageMerger;
use ParaTest\JUnit\LogMerger;
use ParaTest\JUnit\Writer;
use ParaTest\Options;
@@ -25,11 +24,17 @@ use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
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 SplFileInfo;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\PhpExecutableFinder;
use function array_filter;
use function array_merge;
use function array_merge_recursive;
use function array_shift;
@@ -344,6 +349,20 @@ final class WrapperRunner implements RunnerInterface
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueDeprecationEvents(), $testResult->testRunnerTriggeredIssueDeprecationEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueErrorEvents(), $testResult->testRunnerTriggeredIssueErrorEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueNoticeEvents(), $testResult->testRunnerTriggeredIssueNoticeEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssuePhpDeprecationEvents(), $testResult->testRunnerTriggeredIssuePhpDeprecationEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssuePhpNoticeEvents(), $testResult->testRunnerTriggeredIssuePhpNoticeEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssuePhpWarningEvents(), $testResult->testRunnerTriggeredIssuePhpWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->testRunnerTriggeredIssueWarningEvents(), $testResult->testRunnerTriggeredIssueWarningEvents()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
// @phpstan-ignore-next-line
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
@@ -381,6 +400,13 @@ final class WrapperRunner implements RunnerInterface
$testResultSum->testRunnerTriggeredWarningEvents(),
fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found')
)),
$testResultSum->testRunnerTriggeredIssueDeprecationEvents(),
$testResultSum->testRunnerTriggeredIssueErrorEvents(),
$testResultSum->testRunnerTriggeredIssueNoticeEvents(),
$testResultSum->testRunnerTriggeredIssuePhpDeprecationEvents(),
$testResultSum->testRunnerTriggeredIssuePhpNoticeEvents(),
$testResultSum->testRunnerTriggeredIssuePhpWarningEvents(),
$testResultSum->testRunnerTriggeredIssueWarningEvents(),
$testResultSum->errors(),
$testResultSum->deprecations(),
$testResultSum->notices(),
@@ -447,10 +473,33 @@ final class WrapperRunner implements RunnerInterface
return;
}
$coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
foreach ($this->coverageFiles as $coverageFile) {
$coverageMerger->addCoverageFromFile($coverageFile);
$coverageFiles = [];
foreach ($this->coverageFiles as $fileInfo) {
$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(
$this->printer->printer,
@@ -7,6 +7,7 @@ namespace Pest\Plugins\Parallel\Support;
use NunoMaduro\Collision\Adapters\Phpunit\State;
use NunoMaduro\Collision\Adapters\Phpunit\Style;
use ParaTest\Options;
use PHPUnit\Event\Telemetry\CpuTime;
use PHPUnit\Event\Telemetry\GarbageCollectorStatus;
use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Telemetry\Info;
@@ -147,11 +148,20 @@ final class CompactPrinter
MemoryUsage::fromBytes(0),
MemoryUsage::fromBytes(0),
$garbageCollectorStatus,
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
),
$telemetryDuration,
MemoryUsage::fromBytes(0),
\PHPUnit\Event\Telemetry\Duration::fromSecondsAndNanoseconds(0, 0),
MemoryUsage::fromBytes(0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
CpuTime::fromSecondsAndNanoseconds(0, 0),
);
$this->style->writeRecap($state, $telemetry, $testResult);
+105 -12
View File
@@ -27,6 +27,13 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
private const string SHARD_OPTION = 'shard';
/**
* The maximum length allowed for the filter argument.
* While ARG_MAX can be 2MB, individual arguments are often limited to 128KB (MAX_ARG_STRLEN).
* Practical limits in CI environments (like Docker or pipeline runners) can be even lower.
*/
private const int MAX_FILTER_LENGTH = 32768;
/**
* The shard index and total number of shards.
*
@@ -132,7 +139,8 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
self::$timeBalanced = true;
self::$shardsOutdated = $newTests !== [];
} else {
$testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? [];
$isInCurrentShard = fn (int $key): bool => $key % $total === ($index - 1);
$testsToRun = array_values(array_filter($tests, $isInCurrentShard, ARRAY_FILTER_USE_KEY));
}
self::$shard = [
@@ -146,7 +154,11 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
return $arguments;
}
return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)];
$filter = $this->buildFilterArgument($testsToRun);
$this->ensureFilterLengthIsSafe($filter);
return [...$arguments, '--filter', $filter];
}
/**
@@ -187,15 +199,14 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
*/
private function allTests(array $arguments): array
{
$output = (new Process([
'php',
...$this->removeParallelArguments($arguments),
'--list-tests',
]))->setTimeout(120)->mustRun()->getOutput();
$command = $this->buildListTestsCommand(
$arguments,
TestSuite::getInstance()->testPath,
);
preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches);
$output = new Process($command)->setTimeout(120)->mustRun()->getOutput();
return array_values(array_unique($matches[1]));
return $this->parseListTestsOutput($output);
}
/**
@@ -204,15 +215,97 @@ final class Shard implements AddsOutput, HandlesArguments, Terminable
*/
private function removeParallelArguments(array $arguments): array
{
return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true));
return array_values(array_filter(
$arguments,
fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true)
&& ! str_starts_with($argument, '--processes'),
));
}
/**
* Builds the subprocess command used to enumerate tests via `--list-tests`.
*
* @param list<string> $arguments
* @return list<string>
*/
private function buildListTestsCommand(array $arguments, string $testPath): array
{
$filtered = $this->removeParallelArguments($arguments);
return ['php', ...$filtered, '--test-directory='.$testPath, '--list-tests'];
}
/**
* Parses `--list-tests` output into a unique list of test class FQCNs.
*
* @return list<string>
*/
private function parseListTestsOutput(string $output): array
{
preg_match_all('/ - (?:P\\\\)?([A-Za-z_]\w*(?:\\\\[A-Za-z_]\w*)*)::/', $output, $matches);
return array_values(array_unique($matches[1]));
}
/**
* Builds the filter argument for the given tests to run.
*
* @param array<int, string> $testsToRun
*/
private function buildFilterArgument(mixed $testsToRun): string
private function buildFilterArgument(array $testsToRun): string
{
return addslashes(implode('|', $testsToRun));
if ($testsToRun === []) {
return '';
}
/** @var array<string, mixed> $tree */
$tree = [];
foreach ($testsToRun as $class) {
$parts = explode('\\', $class);
$current = &$tree;
foreach ($parts as $part) {
if (! isset($current[$part])) {
$current[$part] = [];
}
$current = &$current[$part];
}
}
$buildRegex = function (array $tree) use (&$buildRegex): string {
$parts = [];
foreach ($tree as $key => $sub) {
$subRegex = $buildRegex($sub);
if ($subRegex === '') {
$parts[] = preg_quote($key, '/');
} else {
$parts[] = preg_quote($key, '/').'\\\\'.(count($sub) > 1 ? '('.$subRegex.')' : $subRegex);
}
}
return implode('|', $parts);
};
return $buildRegex($tree);
}
/**
* Ensures that the filter length is safe for the current environment.
*
* @throws InvalidOption
*/
private function ensureFilterLengthIsSafe(string $filter): void
{
$maxLength = (int) (getenv('PEST_SHARD_MAX_FILTER_LENGTH') ?: self::MAX_FILTER_LENGTH);
if (strlen($filter) > $maxLength) {
throw new InvalidOption(sprintf(
'The generated filter for this shard is too long (%d characters). '.
'This can cause issues with some environments (limit is %d characters). '.
'Please increase the number of shards (e.g., use 1/4 instead of 1/2) to reduce the filter length.',
strlen($filter),
$maxLength
));
}
}
/**
+83 -8
View File
@@ -9,6 +9,7 @@ use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Contracts\Plugins\Terminable;
use Pest\Exceptions\NoAffectedTestsFound;
use Pest\Exceptions\TiaRequiresRepositoryRoot;
use Pest\Panic;
use Pest\Plugins\Concerns\HandleArguments;
use Pest\Plugins\Tia\BaselineSync;
@@ -219,8 +220,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);
}
/**
@@ -251,7 +255,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;
}
}
@@ -292,7 +296,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$result = $this->replayGraph->getResult($this->branch, $testId);
if ($result instanceof TestStatus) {
if ($result->isFailure() || $result->isError()) {
if ($this->replayGraph->shouldRerunStatus($result)) {
$this->executedCount++;
return null;
@@ -398,7 +402,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$projectRoot = TestSuite::getInstance()->rootPath;
$perTest = $this->piggybackCoverage
? $this->coverageCollector->perTestFiles()
? $this->mergePerTestFiles($this->coverageCollector->perTestFiles(), $recorder->perTestFiles())
: $recorder->perTestFiles();
if ($perTest === []) {
@@ -558,7 +562,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return $exitCode;
}
$this->snapshotTestResults();
$this->snapshotTestResults(markKnownTestFiles: true);
return $exitCode;
}
@@ -624,7 +628,14 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private function handleParent(array $arguments, string $projectRoot, bool $forceRebuild): array
{
$this->watchPatterns->useDefaults($projectRoot);
$this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
$subdirectoryPrefix = $this->gitSubdirectoryPrefix($projectRoot);
if ($subdirectoryPrefix !== null) {
Panic::with(new TiaRequiresRepositoryRoot($subdirectoryPrefix));
}
$this->branch = new ChangedFiles($projectRoot)->currentBranch() ?? 'main';
$fingerprint = Fingerprint::compute($projectRoot);
$this->startFingerprint = $fingerprint;
@@ -687,7 +698,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
*/
private function handleWorker(array $arguments, string $projectRoot, bool $recordingGlobal, bool $replayingGlobal): array
{
$this->branch = (new ChangedFiles($projectRoot))->currentBranch() ?? 'main';
$this->branch = new ChangedFiles($projectRoot)->currentBranch() ?? 'main';
if ($replayingGlobal) {
$this->installWorkerReplay($projectRoot);
@@ -704,6 +715,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
if ($this->piggybackCoverage) {
$this->recorder->activateLinkTracking();
$this->recordingActive = true;
return $arguments;
@@ -771,6 +783,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
private function activateWorkerRecorderForReplay(array $arguments): array
{
if ($this->piggybackCoverage) {
$this->recorder->activateLinkTracking();
$this->recordingActive = true;
return $arguments;
@@ -823,6 +836,13 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$affectedFromChanges = $changed === [] ? [] : $graph->affected($changed);
$rerunFromCache = [];
if ($this->filteredMode && $graph->hasUnlocatedTestsToRerun($this->branch)) {
$this->filteredMode = false;
$this->renderBadge('WARN', 'Some cached tests due a re-run could not be located on disk.');
$this->renderChild('Running the full suite with replay instead of a filtered run.');
}
if ($this->filteredMode) {
$rerunFromCache = $graph->testFilesToRerun($this->branch);
}
@@ -1003,6 +1023,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
if ($this->piggybackCoverage) {
$recorder->activateLinkTracking();
$this->recordingActive = true;
$this->output->writeln('');
@@ -1224,6 +1245,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$data = $this->readPartial($key);
if ($data === null) {
$this->state->delete($key);
continue;
}
@@ -1357,6 +1380,26 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
$this->saveGraph($graph);
}
/**
* Union of two per-test edge maps — piggybacked line-coverage edges plus
* the recorder's link-tracked edges (rendered Blade views, ...), which
* never appear in line coverage.
*
* @param array<string, array<int, string>> $coverage
* @param array<string, array<int, string>> $linked
* @return array<string, array<int, string>>
*/
private function mergePerTestFiles(array $coverage, array $linked): array
{
foreach ($linked as $testFile => $sources) {
$existing = $coverage[$testFile] ?? [];
$coverage[$testFile] = array_values(array_unique([...$existing, ...$sources]));
}
return $coverage;
}
private function seedResultsInto(Graph $graph): void
{
/** @var ResultCollector $collector */
@@ -1368,6 +1411,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
foreach ($results as $testId => $result) {
$file = $result['file'] ?? null;
if ($file === null || str_contains($file, "eval()'d")) {
$file = $this->resolveFailedTestFile($testId);
}
if (is_string($file) && $file !== '') {
$touchedFiles[$file] = true;
}
@@ -1383,12 +1430,13 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
);
}
$graph->markKnownTestFiles(array_keys($touchedFiles));
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$collector->reset();
}
private function snapshotTestResults(): void
private function snapshotTestResults(bool $markKnownTestFiles = false): void
{
/** @var ResultCollector $collector */
$collector = Container::getInstance()->get(ResultCollector::class);
@@ -1431,6 +1479,10 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
);
}
if ($markKnownTestFiles) {
$graph->markKnownTestFiles(array_keys($touchedFiles));
}
$graph->pruneStaleResults($this->branch, array_keys($touchedFiles), array_keys($results));
$this->saveGraph($graph);
@@ -1489,6 +1541,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
}
foreach ($arguments as $index => $arg) {
$arg = (string) $arg; // @phpstan-ignore-line
if ($arg === '') {
continue;
}
@@ -1638,6 +1692,27 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
return implode(', ', array_keys($seen));
}
/**
* The path from the git repository root down to $projectRoot (e.g.
* `laravel-app`) when the project is nested inside a larger repo, or `null`
* when the project root is itself the repo root (or git is unavailable).
* TIA requires the two to coincide: git reports and addresses paths
* relative to the repo root, while the dependency graph is project-relative.
*/
private function gitSubdirectoryPrefix(string $projectRoot): ?string
{
$process = new Process(['git', 'rev-parse', '--show-prefix'], $projectRoot);
$process->run();
if (! $process->isSuccessful()) {
return null;
}
$prefix = trim($process->getOutput());
return $prefix === '' ? null : rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $prefix), '/');
}
private function composerLockDelta(string $projectRoot, string $sha): string
{
$current = @file_get_contents($projectRoot.'/composer.lock');
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Tia\Contracts;
/**
* @internal
*/
interface Lockfile
{
public function applies(string $filename): bool;
public function fingerprint(string $contents): ?string;
}
+4
View File
@@ -89,6 +89,10 @@ final class FileState implements State
$keys = [];
foreach ($matches as $path) {
if (str_ends_with($path, '.tmp')) {
continue;
}
$keys[] = basename($path);
}
+45 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Plugins\Tia;
use Pest\Plugins\Tia\Contracts\Lockfile;
use Symfony\Component\Finder\Finder;
/**
@@ -11,7 +12,14 @@ use Symfony\Component\Finder\Finder;
*/
final readonly class Fingerprint
{
private const int SCHEMA_VERSION = 17;
private const int SCHEMA_VERSION = 18;
/**
* @var array<int, class-string<Lockfile>>
*/
private const array LOCKFILES = [
Lockfiles\PackageLock::class,
];
/**
* @return array{
@@ -205,7 +213,11 @@ final readonly class Fingerprint
$parts = [];
foreach (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'] as $name) {
$hash = self::trackedHash($projectRoot, $name);
if (! self::isTrackedByGit($projectRoot, $name)) {
continue;
}
$hash = self::lockfileHash($projectRoot.'/'.$name, $name);
if ($hash !== null) {
$parts[] = $name.':'.$hash;
@@ -215,6 +227,37 @@ final readonly class Fingerprint
return $parts === [] ? null : hash('xxh128', implode("\n", $parts));
}
private static function lockfileHash(string $path, string $name): ?string
{
if (! is_file($path)) {
return null;
}
$contents = @file_get_contents($path);
if ($contents === false) {
return null;
}
foreach (self::LOCKFILES as $class) {
$handler = new $class;
if (! $handler->applies($name)) {
continue;
}
$fingerprint = $handler->fingerprint($contents);
if ($fingerprint !== null) {
return $fingerprint;
}
break;
}
return hash('xxh128', $contents);
}
private static function trackedHash(string $projectRoot, string $relativePath): ?string
{
if (! self::isTrackedByGit($projectRoot, $relativePath)) {
+67 -11
View File
@@ -149,6 +149,13 @@ final class Graph
*/
private function applyMigrationChanges(array $migrationPaths, array &$affectedSet): array
{
// With no recorded table usage at all, table intersection can never
// select anything — route every migration change through the
// watch-pattern fallback instead of silently skipping tests.
if ($this->testTables === []) {
return $migrationPaths;
}
$changedTables = [];
$unparseable = [];
@@ -446,13 +453,14 @@ final class Graph
$bladeAffected = $this->affectedByStaticBladeUsage($rel);
// Only a walk that actually selected tests counts as handled — a
// component whose usage the static walk missed must still reach
// the watch-pattern fallback instead of being silently swallowed.
if ($bladeAffected !== []) {
foreach ($bladeAffected as $testFile) {
$affectedSet[$testFile] = true;
}
$staticallyHandled[$rel] = true;
} elseif ($this->isBladeComponentPath($rel)) {
$staticallyHandled[$rel] = true;
}
}
@@ -690,8 +698,16 @@ final class Graph
private function shouldRerun(int $status): bool
{
$testStatus = TestStatus::from($status);
return $this->shouldRerunStatus(TestStatus::from($status));
}
/**
* Whether a cached result with this status must be re-executed rather
* than replayed, honouring the configured failOn* / displayDetailsOn*
* policies.
*/
public function shouldRerunStatus(TestStatus $testStatus): bool
{
if ($testStatus->isFailure() || $testStatus->isError()) {
return true;
}
@@ -813,6 +829,38 @@ final class Graph
}
}
/**
* Mark test files that executed under a recorded coverage session as "known",
* seeding an empty edge set for any that produced zero project-source edges.
*
* Without this, a test that covers no application source (e.g. a pure unit
* test asserting on language primitives) never becomes an edge key, so
* {@see self::knowsTest()} reports it as unknown and it re-runs on every TIA
* run. Recording it with an empty edge set lets it be replayed/skipped while
* unchanged; it is still re-run whenever its own file changes, via
* {@see self::applyTestFileChanges()}.
*
* Must only be called from the recording path, where coverage was actually
* collected — otherwise a missing edge set could mean "coverage was off",
* not "genuinely covered nothing".
*
* @param array<int, string> $testFiles Absolute or project-relative test file paths.
*/
public function markKnownTestFiles(array $testFiles): void
{
foreach ($testFiles as $testFile) {
$rel = $this->relative($testFile);
if ($rel === null) {
continue;
}
if (! isset($this->edges[$rel])) {
$this->edges[$rel] = [];
}
}
}
/**
* @param array<string, array<int, string>> $testToTables
*/
@@ -1217,7 +1265,21 @@ final class Graph
$tail = substr($tail, 0, -strlen('.blade.php'));
$name = str_replace('/', '.', $tail);
return $name === '' ? [] : [$name, str_replace('_', '-', $name)];
if ($name === '') {
return [];
}
$names = [$name, str_replace('_', '-', $name)];
// Anonymous index components: components/card/index.blade.php resolves as <x-card>.
if (str_ends_with($name, '.index') && $name !== '.index') {
$base = substr($name, 0, -strlen('.index'));
$names[] = $base;
$names[] = str_replace('_', '-', $base);
}
return array_values(array_unique($names));
}
/** @return list<string> */
@@ -1289,13 +1351,7 @@ final class Graph
/** @param array<string, array<int, string>> $edges */
private function anyTestUses(array $edges, string $component): bool
{
foreach ($edges as $components) {
if (in_array($component, $components, true)) {
return true;
}
}
return false;
return array_any($edges, fn (array $components): bool => in_array($component, $components, true));
}
public function pruneMissingTests(): void
+4 -8
View File
@@ -163,7 +163,9 @@ final class JsModuleGraph
return null;
}
if (! is_dir($projectRoot.DIRECTORY_SEPARATOR.'node_modules'.DIRECTORY_SEPARATOR.'vite')) {
$nodeModules = $projectRoot.DIRECTORY_SEPARATOR.'node_modules';
if (! is_dir($nodeModules.DIRECTORY_SEPARATOR.'vite') && ! is_dir($nodeModules.DIRECTORY_SEPARATOR.'rolldown')) {
return null;
}
@@ -386,12 +388,6 @@ final class JsModuleGraph
private static function hasViteConfig(string $projectRoot): bool
{
foreach (self::VITE_CONFIG_NAMES as $name) {
if (is_file($projectRoot.DIRECTORY_SEPARATOR.$name)) {
return true;
}
}
return false;
return array_any(self::VITE_CONFIG_NAMES, fn (string $name): bool => is_file($projectRoot.DIRECTORY_SEPARATOR.$name));
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins\Tia\Lockfiles;
use Pest\Plugins\Tia\Contracts\Lockfile;
/**
* @internal
*/
final readonly class PackageLock implements Lockfile
{
public function applies(string $filename): bool
{
return $filename === 'package-lock.json';
}
public function fingerprint(string $contents): ?string
{
$data = json_decode($contents, true);
if (! is_array($data) || ! isset($data['packages']) || ! is_array($data['packages'])) {
return null;
}
$packages = $data['packages'];
$platformPaths = [];
foreach ($packages as $path => $meta) {
if (is_string($path) && is_array($meta) && $this->isPlatformSpecific($meta)) {
$platformPaths[] = $path;
}
}
$entries = [];
foreach ($packages as $path => $meta) {
if (! is_string($path)) {
continue;
}
if (! is_array($meta)) {
continue;
}
if ($this->isPlatformSpecific($meta)) {
continue;
}
if ($this->isBundledUnderPlatform($path, $platformPaths)) {
continue;
}
$version = $meta['version'] ?? null;
$entries[$path] = is_string($version) ? $version : '';
}
ksort($entries);
$encoded = json_encode($entries, JSON_UNESCAPED_SLASHES);
return $encoded === false ? null : hash('xxh128', $encoded);
}
/**
* @param array<string, mixed> $meta
*/
private function isPlatformSpecific(array $meta): bool
{
return isset($meta['os']) || isset($meta['cpu']) || isset($meta['libc']);
}
/**
* @param list<string> $platformPaths
*/
private function isBundledUnderPlatform(string $path, array $platformPaths): bool
{
return array_any($platformPaths, fn (string $platformPath): bool => str_starts_with($path, $platformPath.'/node_modules/'));
}
}
+39 -3
View File
@@ -34,6 +34,8 @@ final class Recorder
private bool $active = false;
private bool $captureCoverage = false;
private bool $driverChecked = false;
private bool $driverAvailable = false;
@@ -43,6 +45,17 @@ final class Recorder
private ?SourceScope $sourceScope = null;
public function activate(): void
{
$this->active = true;
$this->captureCoverage = true;
}
/**
* Enable per-test link tracking (tables, Inertia components, database
* usage, rendered views) without driving pcov/xdebug — for runs where
* coverage edges are piggybacked from an existing PHPUnit coverage session.
*/
public function activateLinkTracking(): void
{
$this->active = true;
}
@@ -75,7 +88,11 @@ final class Recorder
public function beginTest(string $className, string $methodName, string $fallbackFile): void
{
if (! $this->active || ! $this->driverAvailable()) {
if (! $this->active) {
return;
}
if ($this->captureCoverage && ! $this->driverAvailable()) {
return;
}
@@ -95,6 +112,10 @@ final class Recorder
$this->perTestUsesDatabase[$file] = true;
}
if (! $this->captureCoverage) {
return;
}
if ($this->driver === 'pcov') {
\pcov\clear();
\pcov\start();
@@ -107,7 +128,13 @@ final class Recorder
public function endTest(): void
{
if (! $this->active || ! $this->driverAvailable() || $this->currentTestFile === null) {
if (! $this->active || $this->currentTestFile === null) {
return;
}
if (! $this->captureCoverage || ! $this->driverAvailable()) {
$this->currentTestFile = null;
return;
}
@@ -132,7 +159,15 @@ final class Recorder
$data = \xdebug_get_code_coverage();
\xdebug_stop_code_coverage(true);
$coveredFiles = array_keys($data);
$scope = $this->sourceScope();
foreach (array_keys($data) as $file) {
if (! $scope->contains($file)) {
unset($data[$file]);
}
}
$coveredFiles = $this->filesWithExecutedLines($data);
}
foreach ($coveredFiles as $sourceFile) {
@@ -351,5 +386,6 @@ final class Recorder
$this->classUsesDatabaseCache = [];
$this->sourceScope = null;
$this->active = false;
$this->captureCoverage = false;
}
}
+68 -42
View File
@@ -9,7 +9,12 @@ namespace Pest\Plugins\Tia;
*/
final class TableExtractor
{
private const array DML_PREFIXES = ['select', 'insert', 'update', 'delete'];
private const array DML_PREFIXES = ['select', 'insert', 'update', 'delete', 'with', 'replace'];
/**
* A single (optionally quoted) identifier segment.
*/
private const string IDENTIFIER = '(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|\w+)';
/**
* @return list<string> Sorted, deduped table names referenced by the
@@ -22,22 +27,15 @@ final class TableExtractor
return [];
}
$prefix = strtolower(substr($trimmed, 0, 6));
$matched = false;
foreach (self::DML_PREFIXES as $dml) {
if (str_starts_with($prefix, $dml)) {
$matched = true;
break;
}
}
if (! $matched) {
if (preg_match('/^[a-zA-Z]+/', $trimmed, $prefixMatch) !== 1) {
return [];
}
$pattern = '/(?:\bfrom|\binto|\bupdate|\bjoin)\s+(?:"([^"]+)"|`([^`]+)`|\[([^\]]+)\]|(\w+))/i';
if (! in_array(strtolower($prefixMatch[0]), self::DML_PREFIXES, true)) {
return [];
}
$pattern = '/\b(?:from|into|update|join)\s+('.self::IDENTIFIER.'(?:\s*\.\s*'.self::IDENTIFIER.')*)/i';
if (preg_match_all($pattern, $sql, $matches) === false) {
return [];
@@ -45,14 +43,9 @@ final class TableExtractor
$tables = [];
for ($i = 0, $n = count($matches[0]); $i < $n; $i++) {
$name = $matches[1][$i] !== ''
? $matches[1][$i]
: ($matches[2][$i] !== ''
? $matches[2][$i]
: ($matches[3][$i] !== ''
? $matches[3][$i]
: $matches[4][$i]));
foreach ($matches[1] as $qualified) {
$name = self::unqualified($qualified);
if ($name === '') {
continue;
}
@@ -80,40 +73,40 @@ final class TableExtractor
if (preg_match_all($schemaPattern, $php, $matches) !== false) {
foreach ($matches[1] as $i => $primary) {
$tables[strtolower($primary)] = true;
$tables[strtolower(self::lastDottedSegment($primary))] = true;
$secondary = $matches[2][$i] ?? '';
if ($secondary !== '') {
$tables[strtolower($secondary)] = true;
$tables[strtolower(self::lastDottedSegment($secondary))] = true;
}
}
}
$ddlPattern = '/(?:CREATE|ALTER|DROP|TRUNCATE|RENAME)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?\s+["`\[]?(\w+)["`\]]?/i';
$qualified = '('.self::IDENTIFIER.'(?:\s*\.\s*'.self::IDENTIFIER.')*)';
if (preg_match_all($ddlPattern, $php, $matches) !== false) {
foreach ($matches[1] as $primary) {
$lower = strtolower($primary);
if (! self::isSchemaMeta($lower)) {
$sqlPatterns = [
'/(?:CREATE|ALTER|DROP|TRUNCATE|RENAME)\s+TABLE(?:\s+IF\s+(?:NOT\s+)?EXISTS)?\s+'.$qualified.'/i',
'/INSERT\s+(?:IGNORE\s+)?INTO\s+'.$qualified.'/i',
'/UPDATE\s+'.$qualified.'\s+SET\b/i',
'/DELETE\s+FROM\s+'.$qualified.'/i',
];
foreach ($sqlPatterns as $pattern) {
if (preg_match_all($pattern, $php, $matches) === false) {
continue;
}
foreach ($matches[1] as $name) {
$lower = strtolower(self::unqualified($name));
if ($lower !== '' && ! self::isSchemaMeta($lower)) {
$tables[$lower] = true;
}
}
}
$dmlPatterns = [
'/INSERT\s+(?:IGNORE\s+)?INTO\s+["`\[]?(\w+)["`\]]?/i',
'/UPDATE\s+["`\[]?(\w+)["`\]]?\s+SET\b/i',
'/DELETE\s+FROM\s+["`\[]?(\w+)["`\]]?/i',
'/DB::table\(\s*[\'"]([^\'"]+)[\'"]\s*\)/',
];
foreach ($dmlPatterns as $pattern) {
if (preg_match_all($pattern, $php, $matches) === false) {
continue;
}
if (preg_match_all('/DB::table\(\s*[\'"]([^\'"]+)[\'"]\s*\)/', $php, $matches) !== false) {
foreach ($matches[1] as $name) {
$lower = strtolower($name);
if (! self::isSchemaMeta($lower)) {
$lower = strtolower(self::lastDottedSegment($name));
if ($lower !== '' && ! self::isSchemaMeta($lower)) {
$tables[$lower] = true;
}
}
@@ -125,6 +118,39 @@ final class TableExtractor
return $out;
}
/**
* The table segment of a possibly schema-qualified identifier chain,
* e.g. `"public"."users"` or `analytics.events` yield `users` / `events`.
* Empty when any segment is schema metadata (`information_schema.tables`, ...).
*/
private static function unqualified(string $qualified): string
{
$name = '';
foreach (explode('.', $qualified) as $segment) {
$segment = trim($segment, " \t\n\r\"`[]");
if ($segment === '') {
continue;
}
if (self::isSchemaMeta($segment)) {
return '';
}
$name = $segment;
}
return $name;
}
private static function lastDottedSegment(string $name): string
{
$position = strrpos($name, '.');
return $position === false ? $name : substr($name, $position + 1);
}
private static function isSchemaMeta(string $name): bool
{
$lower = strtolower($name);
+3 -15
View File
@@ -45,11 +45,7 @@ final readonly class TestPaths
$directories[] = $rel;
}
$suffix = $directory->suffix();
if ($suffix !== '') {
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
}
$suffixes[] = $directory->suffix();
}
foreach ($suite->files() as $file) {
@@ -63,7 +59,7 @@ final readonly class TestPaths
if ($suffixes === []) {
foreach ($configuration->testSuffixes() as $suffix) {
$suffixes[] = str_starts_with($suffix, '.') ? $suffix : '.'.$suffix;
$suffixes[] = $suffix;
}
}
} catch (Throwable) {
@@ -94,15 +90,7 @@ final readonly class TestPaths
if (in_array($relativePath, $this->files, true)) {
return true;
}
$matchesSuffix = false;
foreach ($this->suffixes as $suffix) {
if (str_ends_with($relativePath, $suffix)) {
$matchesSuffix = true;
break;
}
}
$matchesSuffix = array_any($this->suffixes, fn (string $suffix): bool => str_ends_with($relativePath, $suffix));
if (! $matchesSuffix) {
return false;
+3 -21
View File
@@ -253,35 +253,17 @@ final class WatchPatterns
private function patternTargetsDotfiles(string $pattern): bool
{
foreach (explode('/', str_replace('\\', '/', $pattern)) as $segment) {
if ($segment !== '' && $segment[0] === '.') {
return true;
}
}
return false;
return array_any(explode('/', str_replace('\\', '/', $pattern)), fn (string $segment): bool => $segment !== '' && $segment[0] === '.');
}
private function touchesVcs(string $file): bool
{
foreach (explode('/', $file) as $segment) {
if (in_array($segment, self::VCS_DIRS, true)) {
return true;
}
}
return false;
return array_any(explode('/', $file), fn (string $segment): bool => in_array($segment, self::VCS_DIRS, true));
}
private function touchesDotfile(string $file): bool
{
foreach (explode('/', $file) as $segment) {
if ($segment !== '' && $segment[0] === '.') {
return true;
}
}
return false;
return array_any(explode('/', $file), fn (string $segment): bool => $segment !== '' && $segment[0] === '.');
}
private function excludeMatches(string $exclude, string $file): bool
+1 -1
View File
@@ -37,7 +37,7 @@ final class XdebugRestarter implements Restarter
return;
}
(new XdebugHandler('pest'))->check();
new XdebugHandler('pest')->check();
}
private function xdebugIsCoverageOnly(): bool
+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();
+10 -1
View File
@@ -9,6 +9,7 @@ use Pest\Plugins\Tia\CoverageMerger;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Report\Facade;
use SebastianBergmann\Environment\Runtime;
use Symfony\Component\Console\Output\OutputInterface;
@@ -95,10 +96,18 @@ final class Coverage
$codeCoverage = require $reportPath;
unlink($reportPath);
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
// @phpstan-ignore-next-line
if (is_array($codeCoverage)) {
$facade = Facade::fromSerializedData($codeCoverage);
/** @var Directory<File|Directory> $report */
$report = (fn (): Directory => $this->report)->call($facade);
} else {
/** @var Directory<File|Directory> $report */
$report = $codeCoverage->getReport();
}
$totalCoverage = $report->percentageOfExecutedLines();
foreach ($report->getIterator() as $file) {
if (! $file instanceof File) {
+13
View File
@@ -86,4 +86,17 @@ final readonly class Exporter
return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->shortenedExport($value));
}
/**
* Exports a value into a full single-line string without truncation.
*/
public function export(mixed $value): string
{
$map = [
'#\\\n\s*#' => '',
'# Object \(\.{3}\)#' => '',
];
return (string) preg_replace(array_keys($map), array_values($map), $this->exporter->export($value));
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ final class HigherOrderMessage
}
if ($this->hasHigherOrderCallable()) {
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
return new HigherOrderCallables($target)->{$this->name}(...$this->arguments);
}
try {
+1 -1
View File
@@ -31,7 +31,7 @@ final class HigherOrderMessageCollection
*/
public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void
{
$this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition);
$this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments)->when($condition);
}
/**
+2 -2
View File
@@ -38,7 +38,7 @@ final class HigherOrderTapProxy
return $this->target->{$property};
}
$className = (new ReflectionClass($this->target))->getName();
$className = new ReflectionClass($this->target)->getName();
if (str_starts_with($className, 'P\\')) {
$className = substr($className, 2);
@@ -60,7 +60,7 @@ final class HigherOrderTapProxy
$filename = Backtrace::file();
$line = Backtrace::line();
return (new HigherOrderMessage($filename, $line, $methodName, $arguments))
return new HigherOrderMessage($filename, $line, $methodName, $arguments)
->call($this->target);
}
}
+2 -2
View File
@@ -181,7 +181,7 @@ final class Reflection
*/
public static function getFunctionArguments(Closure $function): array
{
$parameters = (new ReflectionFunction($function))->getParameters();
$parameters = new ReflectionFunction($function)->getParameters();
$arguments = [];
foreach ($parameters as $parameter) {
@@ -207,7 +207,7 @@ final class Reflection
public static function getFunctionVariable(Closure $function, string $key): mixed
{
return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null;
return new ReflectionFunction($function)->getStaticVariables()[$key] ?? null;
}
/**
+16
View File
@@ -98,6 +98,14 @@ final class Str
return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0;
}
/**
* Determine if a given value is a valid ULID.
*/
public static function isUlid(string $value): bool
{
return preg_match('/^[0-9A-HJKMNP-TV-Z]{26}$/', $value) > 0;
}
/**
* Creates a describe block as `$describeDescription` → `$testDescription` format.
*
@@ -110,6 +118,14 @@ final class Str
return sprintf(str_repeat('`%s` → ', count($describeDescriptions)).'%s', ...$descriptionComponents);
}
/**
* Determine if a given value is a valid email address.
*/
public static function isEmail(string $value): bool
{
return (bool) filter_var($value, FILTER_VALIDATE_EMAIL);
}
/**
* Determine if a given value is a valid URL.
*/
@@ -6,18 +6,18 @@
FAILED Tests\Fixtures\CollisionTest > error Exception
error
at tests/Fixtures/CollisionTest.php:4
1▕ <?php
at tests/Fixtures/CollisionTest.php:6
2▕
3▕ test('error', function () {
4▕ throw new Exception('error');
5▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
6▕
7▕ test('success', function () {
8▕ expect(true)->toBeTrue();
9▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
3▕ declare(strict_types=1);
4▕
5▕ test('error', function (): void {
6▕ throw new Exception('error');
7▕ })->skip(! isset($_SERVER['COLLISION_TEST']));
8▕
9▕ test('success', function (): void {
10▕ expect(true)->toBeTrue();
1 tests/Fixtures/CollisionTest.php:4
1 tests/Fixtures/CollisionTest.php:6
Tests: 1 failed, 1 passed (1 assertions)
@@ -1,5 +1,5 @@
Pest Testing Framework 4.7.2.
Pest Testing Framework 5.0.0-beta.1.
USAGE: pest <file> [options]
@@ -15,6 +15,7 @@
--cache-directory [dir] ............................ Specify cache directory
--generate-configuration Generate configuration file with suggested settings
--migrate-configuration ....... Migrate configuration file to current format
--validate-configuration ................... Validate XML configuration file
--generate-baseline [file] .................... Generate baseline for issues
--use-baseline [file] ........................ Use baseline to ignore issues
--ignore-baseline ..................... Do not use baseline to ignore issues
@@ -40,11 +41,15 @@
--uses [name] ..................... Only run tests that intend to use [name]
--requires-php-extension [name] Only run tests that require PHP extension [name]
--list-test-files ................................ List available test files
--list-test-ids ........................... List available tests as test IDs
--list-tests .......................................... List available tests
--list-tests-xml [file] ................. List available tests in XML format
--filter [pattern] ............................... Filter which tests to run
--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-files-file [file] Only run test files listed in file (one file by line)
--run-test-id [test-id] Only run the test identified by the specified test ID
--test-id-filter-file [file] Only run tests listed by test ID in file (one test ID per line)
EXECUTION OPTIONS:
--parallel ........................................... Run tests in parallel
@@ -53,20 +58,21 @@
--globals-backup ................. Backup and restore $GLOBALS for each test
--static-backup ......... Backup and restore static properties for each test
--strict-coverage ................... Be strict about code coverage metadata
--require-coverage-contribution Be strict about tests that do not contribute to code coverage
--strict-global-state .............. Be strict about changes to global state
--disallow-test-output ................. Be strict about output during tests
--enforce-time-limit ................. Enforce time limit based on test size
--default-time-limit [sec] Timeout in seconds for tests that have no declared size
--do-not-report-useless-tests Do not report tests that do not test anything
--stop-on-defect ... Stop after first error, failure, warning, or risky test
--stop-on-error ..................................... Stop after first error
--stop-on-failure ................................. Stop after first failure
--stop-on-warning ................................. Stop after first warning
--stop-on-risky ................................ Stop after first risky test
--stop-on-deprecation ... Stop after first test that triggered a deprecation
--stop-on-notice ............. Stop after first test that triggered a notice
--stop-on-skipped ............................ Stop after first skipped test
--stop-on-incomplete ...................... Stop after first incomplete test
--stop-on-defect[=[n]] Stop after first (or n-th) error, failure, warning, or risky test
--stop-on-error[=[n]] ..................... Stop after first (or n-th) error
--stop-on-failure[=[n]] ................. Stop after first (or n-th) failure
--stop-on-warning[=[n]] ................. Stop after first (or n-th) warning
--stop-on-risky[=[n]] ................ Stop after first (or n-th) risky test
--stop-on-deprecation[=[n]] Stop after first (or n-th) test that triggered a deprecation
--stop-on-notice[=[n]] Stop after first (or n-th) test that triggered a notice
--stop-on-skipped[=[n]] ............ Stop after first (or n-th) skipped test
--stop-on-incomplete[=[n]] ...... Stop after first (or n-th) incomplete test
--fail-on-empty-test-suite Signal failure using shell exit code when no tests were run
--fail-on-warning Signal failure using shell exit code when a warning was triggered
--fail-on-risky Signal failure using shell exit code when a test was considered risky
@@ -90,7 +96,7 @@
--do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete
--cache-result ............................ Write test results to cache file
--do-not-cache-result .............. Do not write test results to cache file
--order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size
--order-by [order] Run tests in order: default|defects|depends|duration-ascending|duration-descending|no-depends|random|reverse|size-ascending|size-descending
--resolve-dependencies ...................... Alias for "--order-by depends"
--ignore-dependencies .................... Alias for "--order-by no-depends"
--random-order ............................... Alias for "--order-by random"
@@ -101,6 +107,7 @@
--colors=[flag] ......... Use colors in output ("never", "auto" or "always")
--columns [n] ................. Number of columns to use for progress output
--columns max ............ Use maximum number of columns for progress output
--diff-context [n] Number of context lines shown around changes in diffs (default: 3)
--stderr ................................. Write to STDERR instead of STDOUT
--no-progress .................... Disable output of test execution progress
--no-results ................................ Disable output of test results
@@ -115,6 +122,7 @@
--display-warnings ......... Display details for warnings triggered by tests
--display-all-issues ..... Display details for all issues that are triggered
--reverse-list .............................. Print defects in reverse order
--compact ... Replace default progress and result output with compact format
--teamcity . Replace default progress and result output with TeamCity format
--testdox ................ Replace default result output with TestDox format
--testdox-summary Repeat TestDox output for tests with errors, failures, or issues
@@ -125,12 +133,12 @@
LOGGING OPTIONS:
--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
--include-git-information Include Git information in Open Test Reporting XML logfile
--log-teamcity [file] ........ Write test results in TeamCity format 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
--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
--include-git-information ..... Include Git information in supported formats
--no-logging ....... Ignore logging configured in the XML configuration file
CODE COVERAGE OPTIONS:
@@ -151,8 +159,10 @@
--exclude-source-from-xml-coverage Exclude [source] element from code coverage report in XML format
--warm-coverage-cache ........................... Warm static analysis cache
--coverage-filter [dir] ........... Include [dir] in code coverage reporting
--path-coverage .......... Report path coverage in addition to line coverage
--branch-coverage ...... Report branch coverage in addition to line coverage
--path-coverage ... Report path coverage in addition to line/branch coverage
--disable-coverage-ignore ...... Disable metadata for ignoring code coverage
--disable-coverage-targeting .. Disable metadata for code coverage targeting
--no-coverage Ignore code coverage reporting configured in the XML configuration file
AI OPTIONS:
@@ -1,3 +1,3 @@
Pest Testing Framework 4.7.2.
Pest Testing Framework 5.0.0-beta.1.
-28
View File
@@ -1,56 +1,28 @@
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' 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[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[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[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[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[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[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[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[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[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[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[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
Tests: 3 failed, 1 risky, 2 todos, 1 skipped, 1 passed (3 assertions)
Duration: 1.00s
Tests: 3 failed, 1 risky, 2 todos, 1 skipped, 1 passed (3 assertions)
Duration: 1.00s
-19
View File
@@ -1,38 +1,19 @@
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' 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[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[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[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[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[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[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']
Tests: 4 passed (4 assertions)
Duration: 1.00s
Tests: 4 passed (4 assertions)
Duration: 1.00s
+204 -5
View File
@@ -136,6 +136,11 @@
✓ describe()->with() preserves depends → first with (9)
✓ describe()->with() preserves depends → second with (9)
PASS Tests\Features\DatasetProviderErrors
✓ reports missing datasets as errors for a single file run
✓ reports missing datasets as errors alongside passing tests
✓ reports dataset closure exceptions as errors
PASS Tests\Features\DatasetsTests - 1 todo
✓ it throws exception if dataset does not exist
✓ it throws exception if dataset already exist
@@ -293,8 +298,8 @@
✓ it uses correct parent class
DEPR Tests\Features\Deprecated
! deprecated → str_contains(): Passing null to parameter #1 ($haystack) of type string is deprecated // tests/Features/Deprecated.php:4
! user deprecated → Since foo 1.0: This is a deprecation description // tests/Features/Deprecated.php:10
deprecated
! user deprecated → Since foo 1.0: This is a deprecation description // tests/Features/Deprecated.php:12
PASS Tests\Features\Describe - 5 todos
✓ before each
@@ -517,6 +522,13 @@
✓ pass
✓ failures
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeEmail
✓ pass
✓ failures
✓ failures with custom message
✓ failures with default message
✓ not failures
PASS Tests\Features\Expect\toBeEmpty
@@ -752,6 +764,13 @@
✓ passes as not truthy with ('0')
✓ failures
✓ failures with custom message
✓ not failures
PASS Tests\Features\Expect\toBeUlid
✓ failures with wrong type
✓ pass
✓ failures
✓ failures with message
✓ not failures
PASS Tests\Features\Expect\toBeUppercase
@@ -1260,8 +1279,8 @@
// This is another runtime note
NOTI Tests\Features\Notices
! notice → This is a notice description // tests/Features/Notices.php:4
! a "describe" group of tests → notice → This is a notice description // tests/Features/Notices.php:11
! notice → This is a notice description // tests/Features/Notices.php:6
! a "describe" group of tests → notice → This is a notice description // tests/Features/Notices.php:13
PASS Tests\Features\Pr
✓ it may be associated with an pr #1, #2
@@ -1696,6 +1715,8 @@
PASS Tests\Unit\Expectations\OppositeExpectation
✓ it throw expectation failed exception with string 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
✓ collision editor can be added to the stack trace
@@ -1707,6 +1728,8 @@
✓ method hasArgument with ('someValue', true)
✓ method hasArgument with ('--a', false)
✓ method hasArgument with ('--undefined-argument', false)
✓ popArgument preserves duplicate values when removing a missing argument
✓ popArgument preserves duplicate values when removing an existing argument
PASS Tests\Unit\Plugins\Environment
✓ environment is set to CI when --ci option is used
@@ -1715,6 +1738,52 @@
PASS Tests\Unit\Plugins\Retry
✓ it orders by defects and stop on defects if when --retry is used
PASS Tests\Unit\Plugins\Shard
✓ getShard → it parses valid shard format with ('1/2', 1, 2)
✓ getShard → it parses valid shard format with ('2/2', 2, 2)
✓ getShard → it parses valid shard format with ('1/4', 1, 4)
✓ getShard → it parses valid shard format with ('4/4', 4, 4)
✓ getShard → it parses valid shard format with ('1/10', 1, 10)
✓ getShard → it parses valid shard format with ('10/10', 10, 10)
✓ getShard → it parses valid shard format with ('5/100', 5, 100)
✓ getShard → it throws exception for invalid format with (['test', '--shard', 'invalid'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '1'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '1/'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '/2'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', 'a/b'])
✓ getShard → it throws exception for invalid format with (['test', '--shard', '1.5/2'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '0/2'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '1/0'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '3/2'])
✓ getShard → it throws exception for invalid index or total values with (['test', '--shard', '5/4'])
✓ buildFilterArgument → it generates compact filter for single test
✓ buildFilterArgument → it generates compact filter for multiple tests with common prefix
✓ buildFilterArgument → it generates compact filter for tests with different namespaces
✓ buildFilterArgument → it returns empty string for empty test list
✓ buildFilterArgument → it generates compact filter for deeply nested namespaces
✓ buildFilterArgument → it handles mix of nested and flat namespaces
✓ ensureFilterLengthIsSafe → it accepts filter within length limit
✓ ensureFilterLengthIsSafe → it throws exception when filter exceeds default limit
✓ ensureFilterLengthIsSafe → it respects custom limit from environment variable
✓ ensureFilterLengthIsSafe → it accepts filter within custom limit
✓ handleArguments → it returns original arguments when shard option is not present
✓ handleArguments → it removes parallel arguments from test discovery
✓ parseListTestsOutput → it parses Tests\ namespaced classes from --list-tests output
✓ parseListTestsOutput → it deduplicates repeated class names from multiple test methods
✓ parseListTestsOutput → it returns an empty list for output with no matching lines
✓ parseListTestsOutput → it parses non-Tests namespaced classes
✓ parseListTestsOutput → it parses unnamespaced top-level classes
✓ parseListTestsOutput → it strips the P\ Pest prefix but keeps the rest of the FQCN
✓ parseListTestsOutput → it ignores junk lines that lack the " - …::" framing
✓ buildListTestsCommand → it builds the list-tests command with the forwarded --test-directory
✓ buildListTestsCommand → it strips --parallel and -p when building the list-tests command
✓ buildListTestsCommand → it forwards --test-directory even when input arguments include one
✓ buildListTestsCommand → it strips --processes=N when building the list-tests command
✓ buildListTestsCommand → it strips --processes N (space-separated) when building the list-tests command
✓ addOutput → it displays shard information after test execution
✓ addOutput → it uses singular form for single test file
✓ addOutput → it returns original exit code when shard is not set
PASS Tests\Unit\Plugins\Tia\ContentHash
✓ of() → it returns false when file does not exist
✓ of() → it hashes an existing file
@@ -1752,6 +1821,135 @@
✓ output format → it returns a 32-character hex xxh128 hash
✓ output format → it returns a stable hash for empty content
PASS Tests\Unit\Plugins\Tia\FileState
✓ keysWithPrefix() → it lists keys matching the prefix
✓ keysWithPrefix() → it ignores in-flight temporary files from concurrent writes
PASS Tests\Unit\Plugins\Tia\Graph
✓ shouldRerunStatus() → it re-runs failures and errors, replays successes
✓ applyMigrationChanges() → it selects tests whose recorded tables intersect the changed migration
✓ applyMigrationChanges() → it falls back to watch patterns when no table usage was recorded at all
✓ rerun tracking → it reruns cached failures via their file
✓ rerun tracking → it flags cached failures whose file is unknown
✓ applyBladeStaticChanges() → it maps an anonymous index component to the views that render it
✓ applyBladeStaticChanges() → it falls back to watch patterns for components with no matched usage
✓ markKnownTestFiles() → it makes a test file with no edges known
✓ markKnownTestFiles() → it does not clobber edges of an already-known test file
✓ markKnownTestFiles() → it ignores paths outside the project root
PASS Tests\Unit\Plugins\Tia\IsEnabledForRun
✓ does not throw when an integer --random-order-seed is passed as a separate argv element
✓ still detects --tia when an integer argument is present
PASS Tests\Unit\Plugins\Tia\Lockfiles\PackageLock
✓ it applies only to package-lock.json
✓ it returns null for contents that are not an npm lockfile
✓ it is stable when the directory-derived top-level name changes
✓ it ignores platform-specific native binaries added or dropped per OS
✓ it ignores libc-constrained variants
✓ it ignores runtime deps bundled beneath a platform-specific package
✓ it detects a real dependency version change
✓ it detects an added or removed non-platform dependency
✓ it is independent of package ordering
PASS Tests\Unit\Plugins\Tia\Recorder
✓ activateLinkTracking() → it tracks tables without a coverage driver
✓ activateLinkTracking() → it tracks inertia components without a coverage driver
✓ activateLinkTracking() → it tracks linked sources across consecutive tests
✓ activateLinkTracking() → it records nothing while inactive
PASS Tests\Unit\Plugins\Tia\TableExtractor
✓ fromSql() → it extracts tables from plain DML
✓ fromSql() → it extracts tables from joins
✓ fromSql() → it extracts tables from CTE queries
✓ fromSql() → it extracts tables from REPLACE INTO
✓ fromSql() → it records the table, not the schema, for qualified identifiers
✓ fromSql() → it handles quoted identifiers
✓ fromSql() → it ignores schema metadata tables
✓ fromSql() → it returns nothing for non-DML statements
✓ fromMigrationSource() → it extracts tables from Schema builder calls
✓ fromMigrationSource() → it extracts tables from raw DDL statements
✓ fromMigrationSource() → it records the table, not the schema, in qualified DDL and DML
✓ fromMigrationSource() → it extracts tables from DB::table calls
PASS Tests\Unit\Plugins\Tia\TestPaths
✓ isTestFile() → it matches a standard test file under a configured directory
✓ isTestFile() → it does not match a non-test file that shares the directory
✓ isTestFile() → it does not match files outside the configured directories
✓ isTestFile() → it matches an explicitly listed file
✓ isTestFile() → it honours a bare .php suffix
✓ isTestFile() → it does not require a dot before the suffix
PASS Tests\Unit\Plugins\Tia\ViteDepsHelper
✓ it strips JSONC down to something JSON.parse accepts with ('plain-object')
✓ it strips JSONC down to something JSON.parse accepts with ('plain-array')
✓ it strips JSONC down to something JSON.parse accepts with ('nested')
✓ it strips JSONC down to something JSON.parse accepts with ('empty-object')
✓ it strips JSONC down to something JSON.parse accepts with ('empty-array')
✓ it strips JSONC down to something JSON.parse accepts with ('line-comment-own-line')
✓ it strips JSONC down to something JSON.parse accepts with ('line-comment-trailing')
✓ it strips JSONC down to something JSON.parse accepts with ('leading-line-comments')
✓ it strips JSONC down to something JSON.parse accepts with ('line-comment-eof-no-newline')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-leading')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-inline')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-multiline')
✓ it strips JSONC down to something JSON.parse accepts with ('block-comment-eof')
✓ it strips JSONC down to something JSON.parse accepts with ('jsdoc-style-block')
✓ it strips JSONC down to something JSON.parse accepts with ('block-contains-double-slash')
✓ it strips JSONC down to something JSON.parse accepts with ('line-then-block')
✓ it strips JSONC down to something JSON.parse accepts with ('comment-with-quotes')
✓ it strips JSONC down to something JSON.parse accepts with ('comment-with-braces-commas')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-object')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-array')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-nested')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-newline')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-then-block')
✓ it strips JSONC down to something JSON.parse accepts with ('trailing-comma-then-line')
✓ it strips JSONC down to something JSON.parse accepts with ('crlf-line-comment')
✓ it strips JSONC down to something JSON.parse accepts with ('tabs-whitespace')
✓ it strips JSONC down to something JSON.parse accepts with ('url-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('glob-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('path-alias-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-open-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-close-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-both-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('star-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('single-slashes-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('escaped-quote-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('escaped-backslash-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('backslash-then-slash')
✓ it strips JSONC down to something JSON.parse accepts with ('newline-escape-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('unicode-in-string')
✓ it strips JSONC down to something JSON.parse accepts with ('block-secret')
✓ it never touches comment-looking sequences inside string values
✓ it removes the comment body entirely
✓ it builds the expected alias map from a tsconfig with ('basic')
✓ it builds the expected alias map from a tsconfig with ('multiple-aliases')
✓ it builds the expected alias map from a tsconfig with ('default-baseUrl')
✓ it builds the expected alias map from a tsconfig with ('baseUrl-subdir')
✓ it builds the expected alias map from a tsconfig with ('jsconfig-fallback')
✓ it builds the expected alias map from a tsconfig with ('first-target-wins')
✓ it builds the expected alias map from a tsconfig with ('multiple-keys-mixed')
✓ it builds the expected alias map from a tsconfig with ('no-paths')
✓ it builds the expected alias map from a tsconfig with ('no-compiler-options')
✓ it builds the expected alias map from a tsconfig with ('key-without-glob')
✓ it builds the expected alias map from a tsconfig with ('target-without-glob')
✓ it builds the expected alias map from a tsconfig with ('target-not-array')
✓ it builds the expected alias map from a tsconfig with ('target-empty-array')
✓ it builds the expected alias map from a tsconfig with ('commented-tsconfig')
✓ it builds the expected alias map from a tsconfig with ('trailing-comma-paths')
✓ it builds the expected alias map from a tsconfig with ('malformed-json')
✓ it builds the expected alias map from a tsconfig with ('missing-config')
✓ it builds the expected alias map from a tsconfig with ('tsconfig-precedence')
✓ it builds the expected alias map from a vite config with ('root-slash-literal')
✓ it builds the expected alias map from a vite config with ('path-resolve')
✓ it builds the expected alias map from a vite config with ('file-url')
✓ it builds the expected alias map from a vite config with ('tilde-alias')
✓ it builds the expected alias map from a vite config with ('multiple-aliases')
✓ it builds the expected alias map from a vite config with ('laravel-plugin-default')
✓ it builds the expected alias map from a vite config with ('no-alias-no-plugin')
✓ it builds the expected alias map from a vite config with ('no-config')
PASS Tests\Unit\Preset
✓ preset invalid name
✓ preset → myFramework
@@ -1904,6 +2102,7 @@
✓ parallel
✓ a parallel test can extend another test with same name
✓ parallel reports invalid datasets as failures
✓ parallel can have multiple exclude-groups
PASS Tests\Visual\ParallelNestedDatasets
✓ parallel loads nested datasets from nested directories
@@ -1937,4 +2136,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, 1328 passed (3008 assertions)
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1504 passed (3281 assertions)
+9
View File
@@ -0,0 +1,9 @@
<?php
dataset('throws', function () {
throw new RuntimeException('boom from dataset');
});
it('x', function ($a) {
expect($a)->toBeTrue();
})->with('throws');
+7
View File
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
it('references a missing dataset', function ($value) {
expect($value)->toBeTruthy();
})->with('missing');
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
it('passes normally', function () {
expect(true)->toBeTrue();
});
it('references a missing dataset', function ($value) {
expect($value)->toBeTruthy();
})->with('missing');
+1 -1
View File
@@ -21,7 +21,7 @@ trait SecondPluginTrait
Plugin::uses(PluginTrait::class);
Plugin::uses(SecondPluginTrait::class);
function _assertThat()
function _assertThat(): void
{
expect(true)->toBeTrue();
}
+6 -12
View File
@@ -1,19 +1,13 @@
<?php
declare(strict_types=1);
dataset('bound.closure', function () {
yield function () {
return 1;
};
yield function () {
return 2;
};
yield fn (): int => 1;
yield fn (): int => 2;
});
dataset('bound.array', [
function () {
return 1;
},
function () {
return 2;
},
fn (): int => 1,
fn (): int => 2,
]);
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
dataset('numbers.closure', function () {
yield [1];
yield [2];
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
test('global functions are loaded', function () {
declare(strict_types=1);
test('global functions are loaded', function (): void {
expect(helper_returns_string())->toBeString();
});
+25 -25
View File
@@ -1,10 +1,10 @@
<?php
beforeEach(function () {
beforeEach(function (): void {
$this->count = 0;
});
afterEach(function () {
afterEach(function (): void {
match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(1),
'__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(1),
@@ -21,37 +21,37 @@ afterEach(function () {
$this->count++;
});
it('can run after test', function () {
it('can run after test', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(2);
$this->count++;
});
it('can run after test twice', function () {
it('can run after test twice', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(2);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(3);
$this->count++;
});
it('does not run when skipped', function () {
it('does not run when skipped', function (): void {
dd('This should not run 1');
})->skip()->after(function () {
})->skip()->after(function (): void {
dd('This should not run 2');
});
afterEach(function () {
afterEach(function (): void {
match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(3),
'__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(4),
@@ -70,7 +70,7 @@ afterEach(function () {
$this->count++;
});
afterEach(function () {
afterEach(function (): void {
match ($this->name()) {
'__pest_evaluable_it_can_run_after_test' => expect($this->count)->toBe(4),
'__pest_evaluable_it_can_run_after_test_twice' => expect($this->count)->toBe(5),
@@ -87,47 +87,47 @@ afterEach(function () {
$this->count++;
});
describe('something', function () {
it('does not run when skipped', function () {
describe('something', function (): void {
it('does not run when skipped', function (): void {
dd('This should not run 3');
})->skip()->after(function () {
})->skip()->after(function (): void {
dd('This should not run 4');
});
it('can run after test', function () {
it('can run after test', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(5);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(6);
$this->count++;
});
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(4);
$this->count++;
});
describe('something 2', function () {
it('can run after test', function () {
describe('something 2', function (): void {
it('can run after test', function (): void {
expect($this->count)->toBe(0);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(6);
$this->count++;
});
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(4);
$this->count++;
})->after(function () {
})->after(function (): void {
expect($this->count)->toBe(5);
$this->count++;
@@ -136,7 +136,7 @@ describe('something 2', function () {
test('high order test')
->defer(fn () => $this->count++)
->expect(fn () => $this->count)->toBe(1)
->after(function () {
->after(function (): void {
expect($this->count)->toBe(4);
$this->count++;
@@ -146,7 +146,7 @@ test('high order test with skip')
->skip()
->defer(fn () => $this->count++)
->expect(fn () => $this->count)->toBe(1)
->after(function () {
->after(function (): void {
dd('This should not run 5');
});
+4 -4
View File
@@ -2,18 +2,18 @@
$file = __DIR__.DIRECTORY_SEPARATOR.'after-all-test';
beforeAll(function () use ($file) {
beforeAll(function () use ($file): void {
@unlink($file);
});
afterAll(function () use ($file) {
afterAll(function () use ($file): void {
@unlink($file);
});
test('deletes file after all', function () use ($file) {
test('deletes file after all', function () use ($file): void {
file_put_contents($file, 'foo');
$this->assertFileExists($file);
register_shutdown_function(function () {
register_shutdown_function(function (): void {
// $this->assertFileDoesNotExist($file);
});
});
+34 -34
View File
@@ -2,99 +2,99 @@
$state = new stdClass;
beforeEach(function () use ($state) {
beforeEach(function () use ($state): void {
$this->state = $state;
});
afterEach(function () {
afterEach(function (): void {
$this->state->bar = 1;
});
afterEach(function () {
afterEach(function (): void {
unset($this->state->bar);
});
it('does not get executed before the test', function () {
it('does not get executed before the test', function (): void {
expect($this->state)->not->toHaveProperty('bar');
});
it('gets executed after the test', function () {
expect($this->state)->toHaveProperty('bar');
expect($this->state->bar)->toBe(2);
it('gets executed after the test', function (): void {
expect($this->state)->toHaveProperty('bar')
->and($this->state->bar)->toBe(2);
});
afterEach(function () {
afterEach(function (): void {
$this->state->bar = 2;
});
describe('outer', function () {
afterEach(function () {
describe('outer', function (): void {
afterEach(function (): void {
$this->state->bar++;
});
describe('inner', function () {
afterEach(function () {
describe('inner', function (): void {
afterEach(function (): void {
$this->state->bar++;
});
it('does not get executed before the test', function () {
expect($this->state)->toHaveProperty('bar');
expect($this->state->bar)->toBe(2);
it('does not get executed before the test', function (): void {
expect($this->state)->toHaveProperty('bar')
->and($this->state->bar)->toBe(2);
});
it('should call all parent afterEach functions', function () {
expect($this->state)->toHaveProperty('bar');
expect($this->state->bar)->toBe(4);
it('should call all parent afterEach functions', function (): void {
expect($this->state)->toHaveProperty('bar')
->and($this->state->bar)->toBe(4);
});
});
});
describe('matching describe block names', function () {
afterEach(function () {
describe('matching describe block names', function (): void {
afterEach(function (): void {
$this->state->foo = 1;
});
describe('outer', function () {
afterEach(function () {
describe('outer', function (): void {
afterEach(function (): void {
$this->state->foo++;
});
describe('middle', function () {
afterEach(function () {
describe('middle', function (): void {
afterEach(function (): void {
$this->state->foo++;
});
describe('inner', function () {
afterEach(function () {
describe('inner', function (): void {
afterEach(function (): void {
$this->state->foo++;
});
it('does not get executed before the test', function () {
it('does not get executed before the test', function (): void {
expect($this)->not->toHaveProperty('foo');
});
it('should call all parent afterEach functions', function () {
it('should call all parent afterEach functions', function (): void {
expect($this->state->foo)->toBe(4);
});
});
});
describe('middle', function () {
it('does not get executed before the test', function () {
describe('middle', function (): void {
it('does not get executed before the test', function (): void {
expect($this)->not->toHaveProperty('foo');
});
it('should not call afterEach functions for sibling describe blocks with the same name', function () {
it('should not call afterEach functions for sibling describe blocks with the same name', function (): void {
expect($this)->not->toHaveProperty('foo');
});
});
describe('inner', function () {
it('does not get executed before the test', function () {
describe('inner', function (): void {
it('does not get executed before the test', function (): void {
expect($this)->not->toHaveProperty('foo');
});
it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function () {
it('should not call afterEach functions for descendent of sibling describe blocks with the same name', function (): void {
expect($this)->not->toHaveProperty('foo');
});
});
+4 -4
View File
@@ -1,15 +1,15 @@
<?php
beforeEach(function () {
beforeEach(function (): void {
expect(true)->toBeTrue();
})->assignee('nunomaduro');
it('may be associated with an assignee', function () {
it('may be associated with an assignee', function (): void {
expect(true)->toBeTrue();
})->assignee('taylorotwell');
describe('nested', function () {
it('may be associated with an assignee', function () {
describe('nested', function (): void {
it('may be associated with an assignee', function (): void {
expect(true)->toBeTrue();
})->assignee('taylorotwell');
})->assignee('nunomaduro')->note('an note between an the assignee')->assignee(['jamesbrooks', 'joedixon']);
+3 -3
View File
@@ -3,16 +3,16 @@
$foo = new stdClass;
$foo->bar = 0;
beforeAll(function () use ($foo) {
beforeAll(function () use ($foo): void {
$foo->bar++;
});
it('gets executed before tests', function () use ($foo) {
it('gets executed before tests', function () use ($foo): void {
expect($foo)->bar->toBe(1);
$foo->bar = 'changed';
});
it('do not get executed before each test', function () use ($foo) {
it('do not get executed before each test', function () use ($foo): void {
expect($foo)->bar->toBe('changed');
});
+38 -38
View File
@@ -1,91 +1,91 @@
<?php
beforeEach(function () {
beforeEach(function (): void {
$this->bar = 2;
});
beforeEach(function () {
beforeEach(function (): void {
$this->bar++;
});
beforeEach(function () {
beforeEach(function (): void {
$this->bar = 0;
});
it('gets executed before each test', function () {
it('gets executed before each test', function (): void {
expect($this->bar)->toBe(1);
$this->bar = 'changed';
});
it('gets executed before each test once again', function () {
it('gets executed before each test once again', function (): void {
expect($this->bar)->toBe(1);
});
beforeEach(function () {
beforeEach(function (): void {
$this->bar++;
});
describe('outer', function () {
beforeEach(function () {
describe('outer', function (): void {
beforeEach(function (): void {
$this->bar++;
});
describe('inner', function () {
beforeEach(function () {
describe('inner', function (): void {
beforeEach(function (): void {
$this->bar++;
});
it('should call all parent beforeEach functions', function () {
it('should call all parent beforeEach functions', function (): void {
expect($this->bar)->toBe(3);
});
});
});
describe('with expectations', function () {
describe('with expectations', function (): void {
beforeEach()->expect(true)->toBeTrue();
describe('nested block', function () {
test('test', function () {});
describe('nested block', function (): void {
test('test', function (): void {});
});
test('test', function () {});
test('test', function (): void {});
});
describe('matching describe block names', function () {
beforeEach(function () {
describe('matching describe block names', function (): void {
beforeEach(function (): void {
$this->foo = 1;
});
describe('outer', function () {
beforeEach(function () {
describe('outer', function (): void {
beforeEach(function (): void {
$this->foo++;
});
describe('middle', function () {
beforeEach(function () {
describe('middle', function (): void {
beforeEach(function (): void {
$this->foo++;
});
describe('inner', function () {
beforeEach(function () {
describe('inner', function (): void {
beforeEach(function (): void {
$this->foo++;
});
it('should call all parent beforeEach functions', function () {
it('should call all parent beforeEach functions', function (): void {
expect($this->foo)->toBe(4);
});
});
});
describe('middle', function () {
it('should not call beforeEach functions for sibling describe blocks with the same name', function () {
describe('middle', function (): void {
it('should not call beforeEach functions for sibling describe blocks with the same name', function (): void {
expect($this->foo)->toBe(2);
});
});
describe('inner', function () {
it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function () {
describe('inner', function (): void {
it('should not call beforeEach functions for descendent of sibling describe blocks with the same name', function (): void {
expect($this->foo)->toBe(2);
});
});
@@ -93,36 +93,36 @@ describe('matching describe block names', function () {
});
$matchingNameCalls = 0;
describe('matching name', function () use (&$matchingNameCalls) {
beforeEach(function () use (&$matchingNameCalls) {
describe('matching name', function () use (&$matchingNameCalls): void {
beforeEach(function () use (&$matchingNameCalls): void {
$matchingNameCalls++;
});
it('should call the before each', function () use (&$matchingNameCalls) {
it('should call the before each', function () use (&$matchingNameCalls): void {
expect($matchingNameCalls)->toBe(1);
});
});
describe('matching name', function () use (&$matchingNameCalls) {
it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls) {
describe('matching name', function () use (&$matchingNameCalls): void {
it('should not call the before each on the describe block with the same name', function () use (&$matchingNameCalls): void {
expect($matchingNameCalls)->toBe(1);
});
});
beforeEach(function () {
beforeEach(function (): void {
$this->baz = 1;
});
describe('called on all tests', function () {
beforeEach(function () {
describe('called on all tests', function (): void {
beforeEach(function (): void {
$this->baz++;
});
test('beforeEach should be called', function () {
test('beforeEach should be called', function (): void {
expect($this->baz)->toBe(2);
});
test('beforeEach should be called for all tests', function () {
test('beforeEach should be called for all tests', function (): void {
expect($this->baz)->toBe(2);
});
});
@@ -2,14 +2,14 @@
beforeEach()->expect(true)->toBeTrue();
test('runs 1', function () {
test('runs 1', function (): void {
// This test did performs assertions...
});
test('runs 2', function () {
test('runs 2', function (): void {
// This test did performs assertions...
});
test('runs 3', function () {
test('runs 3', function (): void {
// This test did performs assertions...
});
@@ -2,14 +2,14 @@
beforeEach()->skip();
test('does not run 1', function () {
test('does not run 1', function (): void {
$this->fail('This test should not run');
});
test('does not run 2', function () {
test('does not run 2', function (): void {
$this->fail('This test should not run');
});
test('does not run 3', function () {
test('does not run 3', function (): void {
$this->fail('This test should not run');
});
@@ -2,11 +2,11 @@
beforeEach()->todo();
test('is marked as todo 1', function () {
test('is marked as todo 1', function (): void {
$this->fail('This test should not run');
});
test('is marked as todo 2', function () {
test('is marked as todo 2', function (): void {
$this->fail('This test should not run');
});
+4 -4
View File
@@ -6,12 +6,12 @@ use Symfony\Component\Console\Output\ConsoleOutput;
it('has plugin')->assertTrue(class_exists(CoveragePlugin::class));
it('adds coverage if --coverage exist', function () {
it('adds coverage if --coverage exist', function (): void {
$plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverage)->toBeFalse();
$arguments = $plugin->handleArguments([]);
expect($arguments)->toEqual([])
expect($arguments)->toBeEmpty()
->and($plugin->coverage)->toBeFalse();
$arguments = $plugin->handleArguments(['--coverage']);
@@ -19,7 +19,7 @@ it('adds coverage if --coverage exist', function () {
->and($plugin->coverage)->toBeTrue();
})->skip(! Coverage::isAvailable() || ! function_exists('xdebug_info') || ! in_array('coverage', xdebug_info('mode'), true), 'Coverage is not available');
it('adds coverage if --min exist', function () {
it('adds coverage if --min exist', function (): void {
$plugin = new CoveragePlugin(new ConsoleOutput);
expect($plugin->coverageMin)->toEqual(0.0)
->and($plugin->coverage)->toBeFalse();
@@ -34,7 +34,7 @@ it('adds coverage if --min exist', function () {
expect($plugin->coverageMin)->toEqual(2.4);
});
it('generates coverage based on file input', function () {
it('generates coverage based on file input', function (): void {
expect(Coverage::getMissingCoverage(new class
{
public function lineCoverageData(): array
+4 -4
View File
@@ -5,9 +5,9 @@ use Tests\Fixtures\Covers\CoversClass1;
covers([CoversClass1::class]);
it('uses the correct PHPUnit attribute for class', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('uses the correct PHPUnit attribute for class', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class);
expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1');
expect($attributes[1]->getName())->toBe(CoversClass::class)
->and($attributes[1]->getArguments()[0])->toBe(CoversClass1::class);
});
+3 -1
View File
@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
use Pest\PendingCalls\TestCall;
use Pest\TestSuite;
it('throws exception if no class nor method has been found', function () {
it('throws exception if no class nor method has been found', function (): void {
$testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
$testCall->covers('fakeName');
+5 -5
View File
@@ -2,11 +2,11 @@
use PHPUnit\Framework\Attributes\CoversFunction;
function testCoversFunction() {}
function testCoversFunction(): void {}
it('uses the correct PHPUnit attribute for function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('uses the correct PHPUnit attribute for function', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(CoversFunction::class);
expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction');
expect($attributes[1]->getName())->toBe(CoversFunction::class)
->and($attributes[1]->getArguments()[0])->toBe('testCoversFunction');
})->coversFunction('testCoversFunction');
+7 -8
View File
@@ -4,14 +4,13 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversFunction;
use Tests\Fixtures\Covers\CoversClass3;
function testCoversFunction2() {}
function testCoversFunction2(): void {}
it('guesses if the given argument is a class or function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('guesses if the given argument is a class or function', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(CoversClass::class);
expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class);
expect($attributes[2]->getName())->toBe(CoversFunction::class);
expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2');
expect($attributes[1]->getName())->toBe(CoversClass::class)
->and($attributes[1]->getArguments()[0])->toBe(CoversClass3::class)
->and($attributes[2]->getName())->toBe(CoversFunction::class)
->and($attributes[2]->getArguments()[0])->toBe('testCoversFunction2');
})->covers(CoversClass3::class, 'testCoversFunction2');
+4 -4
View File
@@ -3,9 +3,9 @@
use PHPUnit\Framework\Attributes\CoversTrait as PHPUnitCoversTrait;
use Tests\Fixtures\Covers\CoversTrait;
it('uses the correct PHPUnit attribute for trait', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
it('uses the correct PHPUnit attribute for trait', function (): void {
$attributes = new ReflectionClass($this)->getAttributes();
expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class);
expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait');
expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class)
->and($attributes[1]->getArguments()[0])->toBe(CoversTrait::class);
})->coversTrait(CoversTrait::class);
+78 -78
View File
@@ -11,41 +11,41 @@
// beforeEach()->with() inside describe blocks
// ---------------------------------------------------------------
describe('beforeEach()->with() applies dataset to tests', function () {
describe('beforeEach()->with() applies dataset to tests', function (): void {
beforeEach()->with([10]);
test('receives the dataset value', function ($value) {
test('receives the dataset value', function ($value): void {
expect($value)->toBe(10);
});
it('also receives the dataset value in it()', function ($value) {
it('also receives the dataset value in it()', function ($value): void {
expect($value)->toBe(10);
});
});
describe('beforeEach()->with() with multiple dataset values', function () {
describe('beforeEach()->with() with multiple dataset values', function (): void {
beforeEach()->with([1, 2, 3]);
test('receives each value from the dataset', function ($value) {
test('receives each value from the dataset', function ($value): void {
expect($value)->toBeIn([1, 2, 3]);
});
});
describe('beforeEach()->with() with keyed dataset', function () {
describe('beforeEach()->with() with keyed dataset', function (): void {
beforeEach()->with(['first' => [10], 'second' => [20]]);
test('receives keyed dataset values', function ($value) {
test('receives keyed dataset values', function ($value): void {
expect($value)->toBeIn([10, 20]);
});
});
describe('beforeEach()->with() with closure dataset', function () {
describe('beforeEach()->with() with closure dataset', function (): void {
beforeEach()->with(function () {
yield [100];
yield [200];
});
test('receives values from closure dataset', function ($value) {
test('receives values from closure dataset', function ($value): void {
expect($value)->toBeIn([100, 200]);
});
});
@@ -54,30 +54,30 @@ describe('beforeEach()->with() with closure dataset', function () {
// describe()->with() method chaining
// ---------------------------------------------------------------
describe('describe()->with() passes dataset to tests', function () {
test('receives the dataset value', function ($value) {
describe('describe()->with() passes dataset to tests', function (): void {
test('receives the dataset value', function ($value): void {
expect($value)->toBe(42);
});
it('also receives it in it()', function ($value) {
it('also receives it in it()', function ($value): void {
expect($value)->toBe(42);
});
})->with([42]);
describe('describe()->with() with multiple values', function () {
test('receives each value', function ($value) {
describe('describe()->with() with multiple values', function (): void {
test('receives each value', function ($value): void {
expect($value)->toBeIn([5, 10, 15]);
});
})->with([5, 10, 15]);
describe('describe()->with() with keyed dataset', function () {
test('receives keyed values', function ($value) {
describe('describe()->with() with keyed dataset', function (): void {
test('receives keyed values', function ($value): void {
expect($value)->toBeIn([100, 200]);
});
})->with(['alpha' => [100], 'beta' => [200]]);
describe('describe()->with() with closure dataset', function () {
test('receives closure dataset values', function ($value) {
describe('describe()->with() with closure dataset', function (): void {
test('receives closure dataset values', function ($value): void {
expect($value)->toBeIn([7, 14]);
});
})->with(function () {
@@ -89,33 +89,33 @@ describe('describe()->with() with closure dataset', function () {
// Nested describe blocks with datasets
// ---------------------------------------------------------------
describe('outer with dataset', function () {
describe('inner without dataset', function () {
test('inherits outer dataset', function (...$args) {
describe('outer with dataset', function (): void {
describe('inner without dataset', function (): void {
test('inherits outer dataset', function (...$args): void {
expect($args)->toBe([1]);
});
});
})->with([1]);
describe('nested describe blocks with datasets at multiple levels', function () {
describe('level 1', function () {
test('receives level 1 dataset', function (...$args) {
describe('nested describe blocks with datasets at multiple levels', function (): void {
describe('level 1', function (): void {
test('receives level 1 dataset', function (...$args): void {
expect($args)->toBe([10]);
});
describe('level 2', function () {
test('receives datasets from all ancestor levels', function (...$args) {
describe('level 2', function (): void {
test('receives datasets from all ancestor levels', function (...$args): void {
expect($args)->toBe([10, 20]);
});
})->with([20]);
})->with([10]);
});
describe('deeply nested describe with datasets', function () {
describe('a', function () {
describe('b', function () {
describe('c', function () {
test('receives all ancestor datasets', function (...$args) {
describe('deeply nested describe with datasets', function (): void {
describe('a', function (): void {
describe('b', function (): void {
describe('c', function (): void {
test('receives all ancestor datasets', function (...$args): void {
expect($args)->toBe([1, 2, 3]);
});
})->with([3]);
@@ -127,19 +127,19 @@ describe('deeply nested describe with datasets', function () {
// Combining hook datasets with test-level datasets
// ---------------------------------------------------------------
describe('beforeEach()->with() combined with test->with()', function () {
describe('beforeEach()->with() combined with test->with()', function (): void {
beforeEach()->with([10]);
test('receives both datasets as cross product', function ($hookValue, $testValue) {
expect($hookValue)->toBe(10);
expect($testValue)->toBeIn([1, 2]);
test('receives both datasets as cross product', function ($hookValue, $testValue): void {
expect($hookValue)->toBe(10)
->and($testValue)->toBeIn([1, 2]);
})->with([1, 2]);
});
describe('describe()->with() combined with test->with()', function () {
test('receives both datasets', function ($describeValue, $testValue) {
expect($describeValue)->toBe(5);
expect($testValue)->toBeIn([50, 60]);
describe('describe()->with() combined with test->with()', function (): void {
test('receives both datasets', function ($describeValue, $testValue): void {
expect($describeValue)->toBe(5)
->and($testValue)->toBeIn([50, 60]);
})->with([50, 60]);
})->with([5]);
@@ -147,33 +147,33 @@ describe('describe()->with() combined with test->with()', function () {
// beforeEach()->with() combined with beforeEach closure
// ---------------------------------------------------------------
describe('beforeEach closure and beforeEach()->with() coexist', function () {
beforeEach(function () {
describe('beforeEach closure and beforeEach()->with() coexist', function (): void {
beforeEach(function (): void {
$this->setupValue = 'initialized';
});
beforeEach()->with([99]);
test('has both the closure state and dataset', function ($value) {
expect($this->setupValue)->toBe('initialized');
expect($value)->toBe(99);
test('has both the closure state and dataset', function ($value): void {
expect($this->setupValue)->toBe('initialized')
->and($value)->toBe(99);
});
});
describe('beforeEach()->with() does not interfere with closure hooks', function () {
beforeEach(function () {
describe('beforeEach()->with() does not interfere with closure hooks', function (): void {
beforeEach(function (): void {
$this->counter = 1;
});
beforeEach(function () {
beforeEach(function (): void {
$this->counter++;
});
beforeEach()->with([42]);
test('closures run in order and dataset is applied', function ($value) {
expect($this->counter)->toBe(2);
expect($value)->toBe(42);
test('closures run in order and dataset is applied', function ($value): void {
expect($this->counter)->toBe(2)
->and($value)->toBe(42);
});
});
@@ -181,24 +181,24 @@ describe('beforeEach()->with() does not interfere with closure hooks', function
// Dataset isolation between describe blocks
// ---------------------------------------------------------------
describe('first describe with dataset', function () {
describe('first describe with dataset', function (): void {
beforeEach()->with([111]);
test('gets its own dataset', function ($value) {
test('gets its own dataset', function ($value): void {
expect($value)->toBe(111);
});
});
describe('second describe with different dataset', function () {
describe('second describe with different dataset', function (): void {
beforeEach()->with([222]);
test('gets its own dataset, not the sibling', function ($value) {
test('gets its own dataset, not the sibling', function ($value): void {
expect($value)->toBe(222);
});
});
describe('third describe without dataset', function () {
test('has no dataset leaking from siblings', function () {
describe('third describe without dataset', function (): void {
test('has no dataset leaking from siblings', function (): void {
expect(true)->toBeTrue();
});
});
@@ -207,23 +207,23 @@ describe('third describe without dataset', function () {
// describe()->with() combined with beforeEach hooks
// ---------------------------------------------------------------
describe('describe()->with() with beforeEach closure', function () {
beforeEach(function () {
describe('describe()->with() with beforeEach closure', function (): void {
beforeEach(function (): void {
$this->hookRan = true;
});
test('both hook and dataset work', function ($value) {
expect($this->hookRan)->toBeTrue();
expect($value)->toBe(77);
test('both hook and dataset work', function ($value): void {
expect($this->hookRan)->toBeTrue()
->and($value)->toBe(77);
});
})->with([77]);
describe('describe()->with() with afterEach closure', function () {
afterEach(function () {
describe('describe()->with() with afterEach closure', function (): void {
afterEach(function (): void {
expect($this->value)->toBe(88);
});
test('dataset is available and afterEach runs', function ($value) {
test('dataset is available and afterEach runs', function ($value): void {
$this->value = $value;
expect($value)->toBe(88);
});
@@ -233,18 +233,18 @@ describe('describe()->with() with afterEach closure', function () {
// Multiple tests in a describe with beforeEach()->with()
// ---------------------------------------------------------------
describe('multiple tests share the same beforeEach dataset', function () {
describe('multiple tests share the same beforeEach dataset', function (): void {
beforeEach()->with([33]);
test('first test gets the dataset', function ($value) {
test('first test gets the dataset', function ($value): void {
expect($value)->toBe(33);
});
test('second test also gets the dataset', function ($value) {
test('second test also gets the dataset', function ($value): void {
expect($value)->toBe(33);
});
it('third test with it() also gets the dataset', function ($value) {
it('third test with it() also gets the dataset', function ($value): void {
expect($value)->toBe(33);
});
});
@@ -253,21 +253,21 @@ describe('multiple tests share the same beforeEach dataset', function () {
// Nested describe with beforeEach()->with() at inner level
// ---------------------------------------------------------------
describe('outer describe', function () {
beforeEach(function () {
describe('outer describe', function (): void {
beforeEach(function (): void {
$this->outer = true;
});
describe('inner describe with dataset on hook', function () {
describe('inner describe with dataset on hook', function (): void {
beforeEach()->with([55]);
test('inherits outer beforeEach and has inner dataset', function ($value) {
expect($this->outer)->toBeTrue();
expect($value)->toBe(55);
test('inherits outer beforeEach and has inner dataset', function ($value): void {
expect($this->outer)->toBeTrue()
->and($value)->toBe(55);
});
});
test('outer test is unaffected by inner dataset', function () {
test('outer test is unaffected by inner dataset', function (): void {
expect($this->outer)->toBeTrue();
});
});
@@ -276,12 +276,12 @@ describe('outer describe', function () {
// describe()->with() with depends
// ---------------------------------------------------------------
describe('describe()->with() preserves depends', function () {
test('first', function ($value) {
describe('describe()->with() preserves depends', function (): void {
test('first', function ($value): void {
expect($value)->toBe(9);
});
test('second', function ($value) {
test('second', function ($value): void {
expect($value)->toBe(9);
})->depends('first');
})->with([9]);
+46
View File
@@ -0,0 +1,46 @@
<?php
use Symfony\Component\Process\Process;
$run = function (string $target): array {
$process = new Process(
['php', 'bin/pest', $target],
dirname(__DIR__, 2),
['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true', 'PAO_DISABLE' => '1'],
);
$process->run();
return [
'output' => removeAnsiEscapeSequences($process->getOutput().$process->getErrorOutput()),
'code' => $process->getExitCode(),
];
};
test('reports missing datasets as errors for a single file run', function () use ($run): void {
$result = $run('tests/.tests/IssueOnly.php');
expect($result['output'])
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
->toContain('FAILED')
->toContain('Tests: 1 failed')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
test('reports missing datasets as errors alongside passing tests', function () use ($run): void {
$result = $run('tests/.tests/IssueWithPassing.php');
expect($result['output'])
->toContain("A dataset with the name `missing` does not exist. You can create it using `dataset('missing', ['a', 'b']);`.")
->toContain('1 passed')
->toContain('1 failed')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
test('reports dataset closure exceptions as errors', function () use ($run): void {
$result = $run('tests/.tests/DatasetClosureThrows.php');
expect($result['output'])
->toContain('boom from dataset')
->and($result['code'])->not->toBe(0);
})->skipOnWindows();
+139 -181
View File
@@ -5,25 +5,25 @@ use Pest\Exceptions\DatasetDoesNotExist;
use Pest\Plugin;
use Pest\Repositories\DatasetsRepository;
beforeEach(function () {
beforeEach(function (): void {
$this->foo = 'bar';
});
it('throws exception if dataset does not exist', function () {
it('throws exception if dataset does not exist', function (): void {
$this->expectException(DatasetDoesNotExist::class);
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
DatasetsRepository::resolve(['first'], __FILE__);
});
it('throws exception if dataset already exist', function () {
it('throws exception if dataset already exist', function (): void {
DatasetsRepository::set('second', [[]], __DIR__);
$this->expectException(DatasetAlreadyExists::class);
$this->expectExceptionMessage('A dataset with the name `second` already exists in scope ['.__DIR__.'].');
DatasetsRepository::set('second', [[]], __DIR__);
});
it('sets closures', function () {
it('sets closures', function (): void {
DatasetsRepository::set('foo', function () {
yield [1];
}, __DIR__);
@@ -31,18 +31,18 @@ it('sets closures', function () {
expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]);
});
it('sets arrays', function () {
it('sets arrays', function (): void {
DatasetsRepository::set('bar', [[2]], __DIR__);
expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]);
});
it('gets bound to test case object', function ($value) {
it('gets bound to test case object', function ($value): void {
$this->assertTrue(true);
})->with([['a'], ['b']]);
test('it truncates the description', function () {
expect(true)->toBe(true);
test('it truncates the description', function (): void {
expect(true)->toBeTrue();
// it gets tested by the integration test
})->with([str_repeat('Fooo', 10)]);
@@ -51,61 +51,59 @@ $state->text = '';
$datasets = [[1], [2]];
test('lazy datasets', function ($text) use ($state, $datasets) {
test('lazy datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect(in_array([$text], $datasets))->toBe(true);
expect([$text])->toBeIn($datasets);
})->with($datasets);
test('lazy datasets did the job right', function () use ($state) {
test('lazy datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12');
});
test('interpolated :dataset lazy datasets', function ($text) {
test('interpolated :dataset lazy datasets', function ($text): void {
expect(true)->toBeTrue();
})->with($datasets);
$state->text = '';
test('eager datasets', function ($text) use ($state, $datasets) {
test('eager datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with(function () use ($datasets) {
return $datasets;
});
})->with(fn (): array => $datasets);
test('eager datasets did the job right', function () use ($state) {
test('eager datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212');
});
test('lazy registered datasets', function ($text) use ($state, $datasets) {
test('lazy registered datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with('numbers.array');
test('lazy registered datasets did the job right', function () use ($state) {
test('lazy registered datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212');
});
test('eager registered datasets', function ($text) use ($state, $datasets) {
test('eager registered datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with('numbers.closure');
test('eager registered datasets did the job right', function () use ($state) {
test('eager registered datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12121212');
});
test('eager wrapped registered datasets', function ($text) use ($state, $datasets) {
test('eager wrapped registered datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with('numbers.closure.wrapped');
test('eager registered wrapped datasets did the job right', function () use ($state) {
test('eager registered wrapped datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212121212');
});
test('named datasets', function ($text) use ($state, $datasets) {
test('named datasets', function ($text) use ($state, $datasets): void {
$state->text .= $text;
expect($datasets)->toContain([$text]);
})->with([
@@ -113,14 +111,14 @@ test('named datasets', function ($text) use ($state, $datasets) {
'two' => [2],
]);
test('interpolated :dataset named datasets', function ($text) {
test('interpolated :dataset named datasets', function ($text): void {
expect(true)->toBeTrue();
})->with([
'one' => [1],
'two' => [2],
]);
test('named datasets did the job right', function () use ($state) {
test('named datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212');
});
@@ -133,13 +131,13 @@ $namedDatasets = [
new Bar,
];
test('lazy named datasets', function ($text) {
test('lazy named datasets', function ($text): void {
expect(true)->toBeTrue();
})->with($namedDatasets);
$counter = 0;
it('creates unique test case names', function (string $name, Plugin $plugin, bool $bool) use (&$counter) {
it('creates unique test case names', function (string $name, Plugin $plugin, bool $bool) use (&$counter): void {
expect(true)->toBeTrue();
$counter++;
})->with([
@@ -151,73 +149,69 @@ it('creates unique test case names', function (string $name, Plugin $plugin, boo
['Name 1', new Plugin, true],
]);
it('creates unique test case names - count', function () use (&$counter) {
it('creates unique test case names - count', function () use (&$counter): void {
expect($counter)->toBe(6);
});
$datasets_a = [[1], [2]];
$datasets_b = [[3], [4]];
test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b]);
})->with($datasets_a, $datasets_b);
test('lazy multiple datasets did the job right', function () use ($state) {
test('lazy multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12121212121213142324');
});
$state->text = '';
test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
})->with(function () use ($datasets_a) {
return $datasets_a;
})->with(function () use ($datasets_b) {
return $datasets_b;
});
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b]);
})->with(fn (): array => $datasets_a)->with(fn (): array => $datasets_b);
test('eager multiple datasets did the job right', function () use ($state) {
test('eager multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212121212121314232413142324');
});
test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void {
$state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]);
expect($datasets)->toContain([$text_b]);
expect($datasets)->toContain([$text_a])
->toContain([$text_b]);
})->with('numbers.array')->with('numbers.array');
test('lazy registered multiple datasets did the job right', function () use ($state) {
test('lazy registered multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212131423241314232411122122');
});
test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void {
$state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]);
expect($datasets)->toContain([$text_b]);
expect($datasets)->toContain([$text_a])
->toContain([$text_b]);
})->with('numbers.array')->with('numbers.closure');
test('eager registered multiple datasets did the job right', function () use ($state) {
test('eager registered multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('12121212121213142324131423241112212211122122');
});
test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets): void {
$state->text .= $text_a.$text_b;
expect($datasets)->toContain([$text_a]);
expect($datasets)->toContain([$text_b]);
expect($datasets)->toContain([$text_a])
->toContain([$text_b]);
})->with('numbers.closure.wrapped')->with('numbers.closure');
test('eager wrapped registered multiple datasets did the job right', function () use ($state) {
test('eager wrapped registered multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('1212121212121314232413142324111221221112212211122122');
});
test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b]);
})->with([
'one' => [1],
'two' => [2],
@@ -226,18 +220,18 @@ test('named multiple datasets', function ($text_a, $text_b) use ($state, $datase
'four' => [4],
]);
test('named multiple datasets did the job right', function () use ($state) {
test('named multiple datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324');
});
test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b) {
test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b): void {
$state->text .= $text_a.$text_b.$text_c;
expect($datasets_a)->toContain([$text_a]);
expect($datasets_b)->toContain([$text_b]);
expect([5, 6])->toContain($text_c);
expect($datasets_a)->toContain([$text_a])
->and($datasets_b)->toContain([$text_b])
->and([5, 6])->toContain($text_c);
})->with($datasets_a, $datasets_b)->with([5, 6]);
test('more than two datasets did the job right', function () use ($state) {
test('more than two datasets did the job right', function () use ($state): void {
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246');
});
@@ -250,125 +244,97 @@ test(
function (int $text) use (
$wrapped_generator_state,
$wrapped_generator_function_datasets
) {
): void {
$wrapped_generator_state->text .= $text;
expect(in_array($text, $wrapped_generator_function_datasets))->toBe(true);
expect($text)->toBeIn($wrapped_generator_function_datasets);
}
)->with('numbers.generators.wrapped');
test('eager registered wrapped datasets with Generator functions did the job right', function () use ($wrapped_generator_state) {
test('eager registered wrapped datasets with Generator functions did the job right', function () use ($wrapped_generator_state): void {
expect($wrapped_generator_state->text)->toBe('1234');
});
test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description) {
test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description): void {
expect($wrapped_generator_state_with_description)->not->toBeEmpty();
})->with(function () {
yield 'taylor' => 'taylor@laravel.com';
yield 'james' => 'james@laravel.com';
});
it('can resolve a dataset after the test case is available', function ($result) {
it('can resolve a dataset after the test case is available', function ($result): void {
expect($result)->toBe('bar');
})->with([
function () {
return $this->foo;
},
fn () => $this->foo,
[
function () {
return $this->foo;
},
fn () => $this->foo,
],
]);
it('can resolve a dataset after the test case is available with multiple datasets', function (string $result, string $result2) {
it('can resolve a dataset after the test case is available with multiple datasets', function (string $result, string $result2): void {
expect($result)->toBe('bar');
})->with([
function () {
return $this->foo;
},
fn () => $this->foo,
[
function () {
return $this->foo;
},
fn () => $this->foo,
],
], [
function () {
return $this->foo;
},
fn () => $this->foo,
[
function () {
return $this->foo;
},
fn () => $this->foo,
],
]);
it('can resolve a dataset after the test case is available with shared yield sets', function ($result) {
it('can resolve a dataset after the test case is available with shared yield sets', function ($result): void {
expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.closure');
it('can resolve a dataset after the test case is available with shared array sets', function ($result) {
it('can resolve a dataset after the test case is available with shared array sets', function ($result): void {
expect($result)->toBeInt()->toBeLessThan(3);
})->with('bound.array');
it('resolves a potential bound dataset logically', function ($foo, $bar) {
expect($foo)->toBe('foo');
expect($bar())->toBe('bar');
it('resolves a potential bound dataset logically', function ($foo, $bar): void {
expect($foo)->toBe('foo')
->and($bar())->toBe('bar');
})->with([
[
'foo',
function () {
return 'bar';
},
fn (): string => 'bar',
], // This should be passed as a closure because we've passed multiple arguments
]);
it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar) {
expect($foo())->toBe('foo');
expect($bar)->toBe('bar');
it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar): void {
expect($foo())->toBe('foo')
->and($bar)->toBe('bar');
})->with([
[
function () {
return 'foo';
}, 'bar',
fn (): string => 'foo', 'bar',
], // This should be passed as a closure because we've passed multiple arguments
]);
it('will not resolve a closure if it is type hinted as a closure', function (Closure $data) {
it('will not resolve a closure if it is type hinted as a closure', function (Closure $data): void {
expect($data())->toBeString();
})->with([
function () {
return 'foo';
},
function () {
return 'bar';
},
fn (): string => 'foo',
fn (): string => 'bar',
]);
it('will not resolve a closure if it is type hinted as a callable', function (callable $data) {
it('will not resolve a closure if it is type hinted as a callable', function (callable $data): void {
expect($data())->toBeString();
})->with([
function () {
return 'foo';
},
function () {
return 'bar';
},
fn (): string => 'foo',
fn (): string => 'bar',
]);
it('can correctly resolve a bound dataset that returns an array', function (array $data) {
it('can correctly resolve a bound dataset that returns an array', function (array $data): void {
expect($data)->toBe(['foo', 'bar', 'baz']);
})->with([
function () {
return ['foo', 'bar', 'baz'];
},
fn (): array => ['foo', 'bar', 'baz'],
]);
it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz) {
it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz): void {
expect([$foo, $bar, $baz])->toBe(['foo', 'bar', 'baz']);
})->with([
function () {
return ['foo', 'bar', 'baz'];
},
fn (): array => ['foo', 'bar', 'baz'],
]);
todo('forbids to define tests in Datasets dirs and Datasets.php files');
@@ -393,57 +359,57 @@ it('may be used with high order even when bound')
->expect(fn (string $greeting) => $greeting)
->throws(InvalidArgumentException::class);
describe('with on nested describe', function () {
describe('nested', function () {
test('before inner describe block', function (...$args) {
describe('with on nested describe', function (): void {
describe('nested', function (): void {
test('before inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
describe('describe', function () {
it('should include the with value from all parent describe blocks', function (...$args) {
describe('describe', function (): void {
it('should include the with value from all parent describe blocks', function (...$args): void {
expect($args)->toBe([1, 2]);
});
test('should include the with value from all parent describe blocks and the test', function (...$args) {
test('should include the with value from all parent describe blocks and the test', function (...$args): void {
expect($args)->toBe([1, 2, 3]);
})->with([3]);
})->with([2]);
test('after inner describe block', function (...$args) {
test('after inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
})->with([1]);
});
describe('matching describe block names', function () {
describe('outer', function () {
test('before inner describe block', function (...$args) {
describe('matching describe block names', function (): void {
describe('outer', function (): void {
test('before inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
describe('inner', function () {
it('should include the with value from all parent describe blocks', function (...$args) {
describe('inner', function (): void {
it('should include the with value from all parent describe blocks', function (...$args): void {
expect($args)->toBe([1, 2]);
});
test('should include the with value from all parent describe blocks and the test', function (...$args) {
test('should include the with value from all parent describe blocks and the test', function (...$args): void {
expect($args)->toBe([1, 2, 3]);
})->with([3]);
})->with([2]);
describe('inner', function () {
it('should not include the value from the other describe block with the same name', function (...$args) {
describe('inner', function (): void {
it('should not include the value from the other describe block with the same name', function (...$args): void {
expect($args)->toBe([1]);
});
});
test('after inner describe block', function (...$args) {
test('after inner describe block', function (...$args): void {
expect($args)->toBe([1]);
});
})->with([1]);
});
test('after describe block', function (...$args) {
test('after describe block', function (...$args): void {
expect($args)->toBe([5]);
})->with([5]);
@@ -454,64 +420,58 @@ it('may be used with high order after describe block')
dataset('after-describe', ['after']);
test('after describe block with named dataset', function (...$args) {
test('after describe block with named dataset', function (...$args): void {
expect($args)->toBe(['after']);
})->with('after-describe');
test('named parameters match by parameter name', function (string $email, string $name) {
expect($name)->toBe('Taylor');
expect($email)->toBe('taylor@laravel.com');
test('named parameters match by parameter name', function (string $email, string $name): void {
expect($name)->toBe('Taylor')
->and($email)->toBe('taylor@laravel.com');
})->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
]);
test('named parameters work with multiple dataset items', function (string $email, string $name) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('named parameters work with multiple dataset items', function (string $email, string $name): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
['name' => 'James', 'email' => 'james@laravel.com'],
]);
test('named parameters work in different order than closure params', function (string $third, string $first, string $second) {
expect($first)->toBe('a');
expect($second)->toBe('b');
expect($third)->toBe('c');
test('named parameters work in different order than closure params', function (string $third, string $first, string $second): void {
expect($first)->toBe('a')
->and($second)->toBe('b')
->and($third)->toBe('c');
})->with([
['first' => 'a', 'second' => 'b', 'third' => 'c'],
]);
test('named parameters work with named dataset keys', function (string $email, string $name) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('named parameters work with named dataset keys', function (string $email, string $name): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with([
'taylor' => ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
'james' => ['name' => 'James', 'email' => 'james@laravel.com'],
]);
test('named parameters work with closures that should be resolved', function (string $email, string $name) {
expect($name)->toBe('bar');
expect($email)->toBe('bar@example.com');
test('named parameters work with closures that should be resolved', function (string $email, string $name): void {
expect($name)->toBe('bar')
->and($email)->toBe('bar@example.com');
})->with([
[
'name' => function () {
return $this->foo;
},
'email' => function () {
return $this->foo.'@example.com';
},
'name' => fn () => $this->foo,
'email' => fn (): string => $this->foo.'@example.com',
],
]);
test('named parameters work with closure type hints', function (Closure $callback, string $name) {
expect($name)->toBe('Taylor');
expect($callback())->toBe('resolved');
test('named parameters work with closure type hints', function (Closure $callback, string $name): void {
expect($name)->toBe('Taylor')
->and($callback())->toBe('resolved');
})->with([
[
'name' => 'Taylor',
'callback' => function () {
return 'resolved';
},
'callback' => fn (): string => 'resolved',
],
]);
@@ -520,23 +480,21 @@ dataset('named-params-dataset', [
['name' => 'James', 'email' => 'james@laravel.com'],
]);
test('named parameters work with registered datasets', function (string $email, string $name) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('named parameters work with registered datasets', function (string $email, string $name): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with('named-params-dataset');
test('named parameters work with bound closure returning associative array', function (string $email, string $name) {
expect($name)->toBe('bar');
expect($email)->toBe('test@example.com');
test('named parameters work with bound closure returning associative array', function (string $email, string $name): void {
expect($name)->toBe('bar')
->and($email)->toBe('test@example.com');
})->with([
function () {
return ['name' => $this->foo, 'email' => 'test@example.com'];
},
fn (): array => ['name' => $this->foo, 'email' => 'test@example.com'],
]);
test('dataset items can mix named and sequential styles', function (string $name, string $email) {
expect($name)->toBeString();
expect($email)->toContain('@');
test('dataset items can mix named and sequential styles', function (string $name, string $email): void {
expect($name)->toBeString()
->and($email)->toContain('@');
})->with([
['name' => 'Taylor', 'email' => 'taylor@laravel.com'],
['James', 'james@laravel.com'],
+17 -17
View File
@@ -2,34 +2,34 @@
$runCounter = 0;
test('first', function () use (&$runCounter) {
test('first', function () use (&$runCounter): string {
expect(true)->toBeTrue();
$runCounter++;
return 'first';
});
test('second', function () use (&$runCounter) {
test('second', function () use (&$runCounter): string {
expect(true)->toBeTrue();
$runCounter++;
return 'second';
});
test('depends', function () {
test('depends', function (): void {
expect(func_get_args())->toBe(['first', 'second']);
})->depends('first', 'second');
test('depends with ...params', function (string ...$params) {
test('depends with ...params', function (string ...$params): void {
expect(func_get_args())->toBe($params);
})->depends('first', 'second');
test('depends with defined arguments', function (string $first, string $second) {
expect($first)->toBe('first');
expect($second)->toBe('second');
test('depends with defined arguments', function (string $first, string $second): void {
expect($first)->toBe('first')
->and($second)->toBe('second');
})->depends('first', 'second');
test('depends run test only once', function () use (&$runCounter) {
test('depends run test only once', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('first', 'second');
@@ -37,42 +37,42 @@ test('depends run test only once', function () use (&$runCounter) {
it('asserts true is true')->assertTrue(true);
test('depends works with the correct test name')->assertTrue(true)->depends('it asserts true is true');
describe('describe block', function () {
describe('describe block', function (): void {
$runCounter = 0;
test('first in describe', function () use (&$runCounter) {
test('first in describe', function () use (&$runCounter): void {
$runCounter++;
expect(true)->toBeTrue();
});
test('second in describe', function () use (&$runCounter) {
test('second in describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(1);
$runCounter++;
})->depends('first in describe');
test('third in describe', function () use (&$runCounter) {
test('third in describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('second in describe');
describe('nested describe', function () {
describe('nested describe', function (): void {
$runCounter = 0;
test('first in nested describe', function () use (&$runCounter) {
test('first in nested describe', function () use (&$runCounter): void {
$runCounter++;
expect(true)->toBeTrue();
});
test('second in nested describe', function () use (&$runCounter) {
test('second in nested describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(1);
$runCounter++;
})->depends('first in nested describe');
test('third in nested describe', function () use (&$runCounter) {
test('third in nested describe', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('second in nested describe');
});
});
test('depends on test after describe block', function () use (&$runCounter) {
test('depends on test after describe block', function () use (&$runCounter): void {
expect($runCounter)->toBe(2);
})->depends('first', 'second');
+5 -5
View File
@@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase;
class InheritanceTest extends TestCase
{
public function foo()
public function foo(): string
{
return 'bar';
}
@@ -12,11 +12,11 @@ class InheritanceTest extends TestCase
pest()->extend(InheritanceTest::class);
it('is a test', function () {
it('is a test', function (): void {
expect(true)->toBeTrue();
});
it('uses correct parent class', function () {
expect(get_parent_class($this))->toEqual(InheritanceTest::class);
expect($this->foo())->toEqual('bar');
it('uses correct parent class', function (): void {
expect(get_parent_class($this))->toEqual(InheritanceTest::class)
->and($this->foo())->toEqual('bar');
})->depends('it is a test');
+5 -3
View File
@@ -1,12 +1,14 @@
<?php
test('deprecated', function () {
str_contains(null, null);
declare(strict_types=1);
test('deprecated', function (): void {
str_contains('', '');
expect(true)->toBeTrue();
});
test('user deprecated', function () {
test('user deprecated', function (): void {
trigger_error('Since foo 1.0: This is a deprecation description', \E_USER_DEPRECATED);
expect(true)->toBeTrue();
+27 -27
View File
@@ -2,58 +2,58 @@
beforeEach(fn () => $this->count = 1);
test('before each', function () {
test('before each', function (): void {
expect($this->count)->toBe(1);
});
describe('hooks', function () {
beforeEach(function () {
describe('hooks', function (): void {
beforeEach(function (): void {
$this->count++;
});
test('value', function () {
test('value', function (): void {
expect($this->count)->toBe(2);
$this->count++;
});
afterEach(function () {
afterEach(function (): void {
expect($this->count)->toBe(3);
});
});
describe('hooks in different orders', function () {
beforeEach(function () {
describe('hooks in different orders', function (): void {
beforeEach(function (): void {
$this->count++;
});
test('value', function () {
test('value', function (): void {
expect($this->count)->toBe(3);
$this->count++;
});
afterEach(function () {
afterEach(function (): void {
expect($this->count)->toBe(4);
});
beforeEach(function () {
beforeEach(function (): void {
$this->count++;
});
});
test('todo')->todo()->shouldNotRun();
test('previous describable before each does not get applied here', function () {
test('previous describable before each does not get applied here', function (): void {
expect($this->count)->toBe(1);
});
describe('todo on hook', function () {
describe('todo on hook', function (): void {
beforeEach()->todo();
test('should not fail')->shouldNotRun();
test('should run')->expect(true)->toBeTrue();
});
describe('todo on describe', function () {
describe('todo on describe', function (): void {
test('should not fail')->shouldNotRun();
test('should run')->expect(true)->toBeTrue();
@@ -63,48 +63,48 @@ test('should run')->expect(true)->toBeTrue();
test('with', fn ($foo) => expect($foo)->toBe(1))->with([1]);
describe('with on hook', function () {
describe('with on hook', function (): void {
beforeEach()->with([2]);
test('value', function ($foo) {
test('value', function ($foo): void {
expect($foo)->toBe(2);
});
});
describe('with on describe', function () {
test('value', function ($foo) {
describe('with on describe', function (): void {
test('value', function ($foo): void {
expect($foo)->toBe(3);
});
})->with([3]);
describe('depends on describe', function () {
test('foo', function () {
describe('depends on describe', function (): void {
test('foo', function (): void {
expect('foo')->toBe('foo');
});
test('bar', function () {
test('bar', function (): void {
expect('bar')->toBe('bar');
})->depends('foo');
});
describe('depends on describe using with', function () {
test('foo', function ($foo) {
describe('depends on describe using with', function (): void {
test('foo', function ($foo): void {
expect($foo)->toBe(3);
});
test('bar', function ($foo) {
test('bar', function ($foo): void {
expect($foo + $foo)->toBe(6);
})->depends('foo');
})->with([3]);
describe('with test after describe', function () {
beforeEach(function () {
describe('with test after describe', function (): void {
beforeEach(function (): void {
$this->count++;
});
describe('foo', function () {});
describe('foo', function (): void {});
it('should run the before each', function () {
it('should run the before each', function (): void {
expect($this->count)->toBe(2);
});
});
+1 -1
View File
@@ -30,7 +30,7 @@ get('foo'); // not incomplete because closure is created...
get('foo')->get('bar')->expect(true)->toBeTrue();
get('foo')->expect(true)->toBeTrue();
describe('a "describe" group of tests', function () {
describe('a "describe" group of tests', function (): void {
get('foo'); // not incomplete because closure is created...
get('foo')->get('bar')->expect(true)->toBeTrue();
get('foo')->expect(true)->toBeTrue();
+6 -4
View File
@@ -1,17 +1,19 @@
<?php
it('may have an associated assignee', function () {
declare(strict_types=1);
it('may have an associated assignee', function (): void {
expect(true)->toBeTrue();
})->done(assignee: 'nunomaduro');
it('may have an associated issue', function () {
it('may have an associated issue', function (): void {
expect(true)->toBeTrue();
})->done(issue: 1);
it('may have an associated PR', function () {
it('may have an associated PR', function (): void {
expect(true)->toBeTrue();
})->done(pr: 1);
it('may have an associated note', function () {
it('may have an associated note', function (): void {
expect(true)->toBeTrue();
})->done(note: 'a note');
+24 -28
View File
@@ -1,95 +1,91 @@
<?php
it('gives access the the underlying expectException', function () {
it('gives access the the underlying expectException', function (): void {
$this->expectException(InvalidArgumentException::class);
throw new InvalidArgumentException;
});
it('catch exceptions', function () {
it('catch exceptions', function (): void {
throw new Exception('Something bad happened');
})->throws(Exception::class);
it('catch exceptions and messages', function () {
it('catch exceptions and messages', function (): void {
throw new Exception('Something bad happened');
})->throws(Exception::class, 'Something bad happened');
it('catch exceptions, messages and code', function () {
it('catch exceptions, messages and code', function (): void {
throw new Exception('Something bad happened', 1);
})->throws(Exception::class, 'Something bad happened', 1);
it('can just define the message', function () {
it('can just define the message', function (): void {
throw new Exception('Something bad happened');
})->throws('Something bad happened');
it('can just define the code', function () {
it('can just define the code', function (): void {
throw new Exception('Something bad happened', 1);
})->throws(1);
it('not catch exceptions if given condition is false', function () {
it('not catch exceptions if given condition is false', function (): void {
$this->assertTrue(true);
})->throwsIf(false, Exception::class);
it('catch exceptions if given condition is true', function () {
it('catch exceptions if given condition is true', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(function () {
return true;
}, Exception::class);
})->throwsIf(fn (): true => true, Exception::class);
it('catch exceptions and messages if given condition is true', function () {
it('catch exceptions and messages if given condition is true', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(true, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is true', function () {
it('catch exceptions, messages and code if given condition is true', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsIf(true, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is true', function () {
it('can just define the message if given condition is true', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(true, 'Something bad happened');
it('can just define the code if given condition is true', function () {
it('can just define the code if given condition is true', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsIf(true, 1);
it('can just define the message if given condition is 1', function () {
it('can just define the message if given condition is 1', function (): void {
throw new Exception('Something bad happened');
})->throwsIf(1, 'Something bad happened');
it('can just define the code if given condition is 1', function () {
it('can just define the code if given condition is 1', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsIf(1, 1);
it('not catch exceptions if given condition is true', function () {
it('not catch exceptions if given condition is true', function (): void {
$this->assertTrue(true);
})->throwsUnless(true, Exception::class);
it('catch exceptions if given condition is false', function () {
it('catch exceptions if given condition is false', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(function () {
return false;
}, Exception::class);
})->throwsUnless(fn (): false => false, Exception::class);
it('catch exceptions and messages if given condition is false', function () {
it('catch exceptions and messages if given condition is false', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(false, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is false', function () {
it('catch exceptions, messages and code if given condition is false', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsUnless(false, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is false', function () {
it('can just define the message if given condition is false', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(false, 'Something bad happened');
it('can just define the code if given condition is false', function () {
it('can just define the code if given condition is false', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsUnless(false, 1);
it('can just define the message if given condition is 0', function () {
it('can just define the message if given condition is 0', function (): void {
throw new Exception('Something bad happened');
})->throwsUnless(0, 'Something bad happened');
it('can just define the code if given condition is 0', function () {
it('can just define the code if given condition is 0', function (): void {
throw new Exception('Something bad happened', 1);
})->throwsUnless(0, 1);
+23 -24
View File
@@ -1,73 +1,72 @@
<?php
it('can access methods', function () {
it('can access methods', function (): void {
expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods');
});
it('can access multiple methods', function () {
it('can access multiple methods', function (): void {
expect(new HasMethods)
->name()->toBeString()->toEqual('Has Methods')
->quantity()->toBeInt()->toEqual(20);
});
it('works with not', function () {
it('works with not', function (): void {
expect(new HasMethods)
->name()->not->toEqual('world')->toEqual('Has Methods')
->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull;
});
it('can accept arguments', function () {
it('can accept arguments', function (): void {
expect(new HasMethods)
->multiply(5, 4)->toBeInt->toEqual(20);
});
it('works with each', function () {
it('works with each', function (): void {
expect(new HasMethods)
->attributes()->toBeArray->each->not()->toBeNull
->attributes()->each(function ($attribute) {
->attributes()->each(function ($attribute): void {
$attribute->not->toBeNull();
});
});
it('works inside of each', function () {
it('works inside of each', function (): void {
expect(new HasMethods)
->books()->each(function ($book) {
->books()->each(function ($book): void {
$book->title->not->toBeNull->cost->toBeGreaterThan(19);
});
});
it('works with sequence', function () {
it('works with sequence', function (): void {
expect(new HasMethods)
->books()->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can compose complex expectations', function () {
it('can compose complex expectations', function (): void {
expect(new HasMethods)
->toBeObject()
->name()->toEqual('Has Methods')->not()->toEqual('bar')
->quantity()->not->toEqual('world')->toEqual(20)->toBeInt
->multiply(3, 4)->not->toBeString->toEqual(12)
->attributes()->toBeArray()
->books()->toBeArray->each->not->toBeEmpty
->books()->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can handle nested method calls', function () {
it('can handle nested method calls', function (): void {
expect(new HasMethods)
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
->newInstance()->name()->toEqual('Has Methods')->not->toBeInt
@@ -82,7 +81,7 @@ it('works with higher order tests')
->name()->toEqual('Has Methods')
->books()->each->toBeArray;
it('can use the scoped method to lock into the given level for expectations', function () {
it('can use the scoped method to lock into the given level for expectations', function (): void {
expect(new HasMethods)
->attributes()->scoped(fn ($attributes) => $attributes
->name->toBe('Has Methods')
@@ -99,7 +98,7 @@ it('can use the scoped method to lock into the given level for expectations', fu
);
});
it('works consistently with the json expectation method', function () {
it('works consistently with the json expectation method', function (): void {
expect(new HasMethods)
->jsonString()->json()->id->toBe(1)
->jsonString()->json()->name->toBe('Has Methods')->toBeString()
@@ -113,22 +112,22 @@ class HasMethods
return '{ "id": 1, "name": "Has Methods", "quantity": 20 }';
}
public function name()
public function name(): string
{
return 'Has Methods';
}
public function quantity()
public function quantity(): int
{
return 20;
}
public function multiply($x, $y)
public function multiply($x, $y): int|float
{
return $x * $y;
}
public function attributes()
public function attributes(): array
{
return [
'name' => $this->name(),
@@ -136,7 +135,7 @@ class HasMethods
];
}
public function books()
public function books(): array
{
return [
[
@@ -150,7 +149,7 @@ class HasMethods
];
}
public function newInstance()
public function newInstance(): static
{
return new static;
}
@@ -1,24 +1,24 @@
<?php
it('can access methods and properties', function () {
it('can access methods and properties', function (): void {
expect(new HasMethodsAndProperties)
->name->toEqual('Has Methods and Properties')->not()->toEqual('bar')
->multiply(3, 4)->not->toBeString->toEqual(12)
->posts->each(function ($post) {
->posts->each(function ($post): void {
$post->is_published->toBeTrue;
})->books()->toBeArray()
->posts->toBeArray->each->not->toBeEmpty
->books()->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can handle nested methods and properties', function () {
it('can handle nested methods and properties', function (): void {
expect(new HasMethodsAndProperties)
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
->newInstance()->meta->foo->toBeArray()
@@ -33,15 +33,14 @@ it('works with higher order tests')
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
->newInstance()->books()->toBeArray();
it('can start a new higher order expectation using the and syntax', function () {
it('can start a new higher order expectation using the and syntax', function (): void {
expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class)
->meta->toBeArray
->and(['foo' => 'bar'])
->toBeArray()
->foo->toEqual('bar');
expect(static::getCount())->toEqual(4);
->foo->toEqual('bar')
->and(static::getCount())->toEqual(4);
});
it('can start a new higher order expectation using the and syntax in higher order tests')
@@ -52,12 +51,12 @@ it('can start a new higher order expectation using the and syntax in higher orde
->toBeArray()
->foo->toEqual('bar');
it('can start a new higher order expectation using the and syntax without nesting expectations', function () {
it('can start a new higher order expectation using the and syntax without nesting expectations', function (): void {
expect(new HasMethodsAndProperties)
->toBeInstanceOf(HasMethodsAndProperties::class)
->meta
->sequence(
function ($value, $key) {
function ($value, $key): void {
$value->toBeArray()->and($key)->toBe('foo');
},
);
@@ -80,7 +79,7 @@ class HasMethodsAndProperties
],
];
public function books()
public function books(): array
{
return [
[
@@ -94,12 +93,12 @@ class HasMethodsAndProperties
];
}
public function multiply($x, $y)
public function multiply($x, $y): int|float
{
return $x * $y;
}
public function newInstance()
public function newInstance(): static
{
return new static;
}
@@ -1,50 +1,50 @@
<?php
it('allows properties to be accessed from the value', function () {
it('allows properties to be accessed from the value', function (): void {
expect(['foo' => 1])->foo->toBeInt()->toEqual(1);
});
it('can access multiple properties from the value', function () {
it('can access multiple properties from the value', function (): void {
expect(['foo' => 'bar', 'hello' => 'world'])
->foo->toBeString()->toEqual('bar')
->hello->toBeString()->toEqual('world');
});
it('works with not', function () {
it('works with not', function (): void {
expect(['foo' => 'bar', 'hello' => 'world'])
->foo->not->not->toEqual('bar')
->foo->not->toEqual('world')->toEqual('bar')
->hello->toEqual('world')->not()->toEqual('bar')->not->toBeNull;
});
it('works with each', function () {
it('works with each', function (): void {
expect(['numbers' => [1, 2, 3, 4], 'words' => ['hey', 'there']])
->numbers->toEqual([1, 2, 3, 4])->each->toBeInt->toBeLessThan(5)
->words->each(function ($word) {
->words->each(function ($word): void {
$word->toBeString()->not->toBeInt();
});
});
it('works inside of each', function () {
it('works inside of each', function (): void {
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
->books->each(function ($book) {
->books->each(function ($book): void {
$book->title->not->toBeNull->cost->toBeGreaterThan(19);
});
});
it('works with sequence', function () {
it('works with sequence', function (): void {
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
->books->sequence(
function ($book) {
function ($book): void {
$book->title->toEqual('Foo')->cost->toEqual(20);
},
function ($book) {
function ($book): void {
$book->title->toEqual('Bar')->cost->toEqual(30);
},
);
});
it('can compose complex expectations', function () {
it('can compose complex expectations', function (): void {
expect(['foo' => 'bar', 'numbers' => [1, 2, 3, 4]])
->toContain('bar')->toBeArray()
->numbers->toEqual([1, 2, 3, 4])->not()->toEqual('bar')->each->toBeInt
@@ -52,23 +52,23 @@ it('can compose complex expectations', function () {
->numbers->toBeArray();
});
it('works with objects', function () {
it('works with objects', function (): void {
expect(new HasProperties)
->name->toEqual('foo')->not->toEqual('world')
->posts->toHaveCount(2)->each(function ($post) {
->posts->toHaveCount(2)->each(function ($post): void {
$post->is_published->toBeTrue();
})
->posts->sequence(
function ($post) {
function ($post): void {
$post->title->toEqual('Foo');
},
function ($post) {
function ($post): void {
$post->title->toEqual('Bar');
},
);
});
it('works with nested properties', function () {
it('works with nested properties', function (): void {
expect(new HasProperties)
->nested->foo->bar->toBeString()->toEqual('baz')
->posts->toBeArray()->toHaveCount(2);
+24 -30
View File
@@ -2,16 +2,15 @@
use Pest\Expectation;
test('an exception is thrown if the the type is not iterable', function () {
test('an exception is thrown if the the type is not iterable', function (): void {
expect('Foobar')->each()->toEqual('Foobar');
})->throws(BadMethodCallException::class, 'Expectation value is not iterable.');
it('expects on each item', function () {
it('expects on each item', function (): void {
expect([1, 1, 1])
->each()
->toEqual(1);
expect(static::getCount())->toBe(3); // + 1 assertion
->toEqual(1)
->and(static::getCount())->toBe(3); // + 1 assertion
expect([1, 1, 1])
->each
@@ -20,13 +19,12 @@ it('expects on each item', function () {
expect(static::getCount())->toBe(7);
});
it('chains expectations on each item', function () {
it('chains expectations on each item', function (): void {
expect([1, 1, 1])
->each()
->toBeInt()
->toEqual(1);
expect(static::getCount())->toBe(6); // + 1 assertion
->toEqual(1)
->and(static::getCount())->toBe(6); // + 1 assertion
expect([2, 2, 2])
->each
@@ -36,13 +34,12 @@ it('chains expectations on each item', function () {
expect(static::getCount())->toBe(13);
});
test('opposite expectations on each item', function () {
test('opposite expectations on each item', function (): void {
expect([1, 2, 3])
->each()
->not()
->toEqual(4);
expect(static::getCount())->toBe(3);
->toEqual(4)
->and(static::getCount())->toBe(3);
expect([1, 2, 3])
->each()
@@ -51,17 +48,16 @@ test('opposite expectations on each item', function () {
expect(static::getCount())->toBe(7);
});
test('chained opposite and non-opposite expectations', function () {
test('chained opposite and non-opposite expectations', function (): void {
expect([1, 2, 3])
->each()
->not()
->toEqual(4)
->toBeInt();
expect(static::getCount())->toBe(6);
->toBeInt()
->and(static::getCount())->toBe(6);
});
it('can add expectations via "and"', function () {
it('can add expectations via "and"', function (): void {
expect([1, 2, 3])
->each()
->toBeInt // + 3
@@ -78,20 +74,18 @@ it('can add expectations via "and"', function () {
expect(static::getCount())->toBe(14);
});
it('accepts callables', function () {
expect([1, 2, 3])->each(function ($number) {
expect($number)->toBeInstanceOf(Expectation::class);
expect($number->value)->toBeInt();
it('accepts callables', function (): void {
expect([1, 2, 3])->each(function ($number): void {
expect($number)->toBeInstanceOf(Expectation::class)
->and($number->value)->toBeInt();
$number->toBeInt->not->toBeString;
})
->and(static::getCount())->toBe(12);
});
expect(static::getCount())->toBe(12);
});
it('passes the key of the current item to callables', function () {
expect([1, 2, 3])->each(function ($number, $key) {
it('passes the key of the current item to callables', function (): void {
expect([1, 2, 3])->each(function ($number, $key): void {
expect($key)->toBeInt();
});
expect(static::getCount())->toBe(3);
})
->and(static::getCount())->toBe(3);
});
+6 -6
View File
@@ -1,29 +1,29 @@
<?php
expect()->extend('toBeAMacroExpectation', function () {
expect()->extend('toBeAMacroExpectation', function (): object {
$this->toBeTrue();
return $this;
});
expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value) {
expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value): object {
$this->toBe($value);
return $this;
});
it('macros true is true', function () {
it('macros true is true', function (): void {
expect(true)->toBeAMacroExpectation();
});
it('macros false is not true', function () {
it('macros false is not true', function (): void {
expect(false)->not->toBeAMacroExpectation();
});
it('macros true is true with argument', function () {
it('macros true is true with argument', function (): void {
expect(true)->toBeAMacroExpectationWithArguments(true);
});
it('macros false is not true with argument', function () {
it('macros false is not true with argument', function (): void {
expect(false)->not->toBeAMacroExpectationWithArguments(true);
});
+2 -2
View File
@@ -2,13 +2,13 @@
use PHPUnit\Framework\ExpectationFailedException;
test('it properly parses json string', function () {
test('it properly parses json string', function (): void {
expect('{"name":"Nuno"}')
->json()
->name
->toBe('Nuno');
});
test('fails with broken json string', function () {
test('fails with broken json string', function (): void {
expect('{":"Nuno"}')->json();
})->throws(ExpectationFailedException::class);
+26 -45
View File
@@ -2,11 +2,11 @@
use PHPUnit\Framework\ExpectationFailedException;
beforeEach(function () {
beforeEach(function (): void {
$this->matched = null;
});
it('pass', function () {
it('pass', function (): void {
expect('baz')
->match('foo', [
'bar' => function ($value) {
@@ -21,25 +21,20 @@ it('pass', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('failures', function () {
it('failures', function (): void {
expect(true)
->match('foo', [
'bar' => function ($value) {
return $value->toBeTrue();
},
'foo' => function ($value) {
return $value->toBeFalse();
},
'bar' => fn ($value) => $value->toBeTrue(),
'foo' => fn ($value) => $value->toBeFalse(),
]
);
})->throws(ExpectationFailedException::class, 'true is false');
it('runs with truthy', function () {
it('runs with truthy', function (): void {
expect('foo')
->match(1, [
'bar' => function ($value) {
@@ -54,12 +49,11 @@ it('runs with truthy', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('runs with falsy', function () {
it('runs with falsy', function (): void {
expect('foo')
->match(false, [
'bar' => function ($value) {
@@ -74,17 +68,14 @@ it('runs with falsy', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('runs with truthy closure condition', function () {
it('runs with truthy closure condition', function (): void {
expect('foo')
->match(
function () {
return '1';
}, [
fn (): string => '1', [
'bar' => function ($value) {
$this->matched = 'bar';
@@ -97,17 +88,14 @@ it('runs with truthy closure condition', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('runs with falsy closure condition', function () {
it('runs with falsy closure condition', function (): void {
expect('foo')
->match(
function () {
return '0';
}, [
fn (): string => '0', [
'bar' => function ($value) {
$this->matched = 'bar';
@@ -120,12 +108,11 @@ it('runs with falsy closure condition', function () {
},
]
)
->toEqual($this->matched);
expect(static::getCount())->toBe(2);
->toEqual($this->matched)
->and(static::getCount())->toBe(2);
});
it('can be passed non-callable values', function () {
it('can be passed non-callable values', function (): void {
expect('foo')
->match('pest', [
'bar' => 'foo',
@@ -134,21 +121,15 @@ it('can be passed non-callable values', function () {
);
})->throws(ExpectationFailedException::class, 'two strings are equal');
it('fails with unhandled match', function () {
it('fails with unhandled match', function (): void {
expect('foo')->match('bar', []);
})->throws(ExpectationFailedException::class, 'Unhandled match value.');
it('can be used in higher order tests')
->expect(true)
->match(
function () {
return true;
}, [
false => function ($value) {
return $value->toBeFalse();
},
true => function ($value) {
return $value->toBeTrue();
},
fn (): true => true, [
false => fn ($value) => $value->toBeFalse(),
true => fn ($value) => $value->toBeTrue(),
]
);
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
test('not property calls', function () {
test('not property calls', function (): void {
expect(true)
->toBeTrue()
->not()->toBeFalse()
+15 -15
View File
@@ -61,7 +61,7 @@ $state = new State;
/*
* Overrides toBe to assert two Characters are the same
*/
expect()->pipe('toBe', function ($next, $expected) use ($state) {
expect()->pipe('toBe', function ($next, $expected) use ($state): void {
$state->runCount['char']++;
if ($this->value instanceof Char) {
@@ -81,7 +81,7 @@ expect()->pipe('toBe', function ($next, $expected) use ($state) {
/*
* Overrides toBe to assert two Number objects are the same
*/
expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
expect()->intercept('toBe', Number::class, function ($expected) use ($state): void {
$state->runCount['number']++;
$state->appliedCount['number']++;
@@ -92,7 +92,7 @@ expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
/*
* Overrides toBe to assert all integers are allowed if value is a wildcard (*)
*/
expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numeric($expected), function ($expected) use ($state) {
expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numeric($expected), function ($expected) use ($state): void {
$state->runCount['wildcard']++;
$state->appliedCount['wildcard']++;
});
@@ -100,7 +100,7 @@ expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numer
/*
* Overrides toBe to assert to Symbols are the same
*/
expect()->pipe('toBe', function ($next, $expected) use ($state) {
expect()->pipe('toBe', function ($next, $expected) use ($state): void {
$state->runCount['symbol']++;
if ($this->value instanceof Symbol) {
@@ -117,7 +117,7 @@ expect()->pipe('toBe', function ($next, $expected) use ($state) {
/*
* Overrides toBe to allow ignoring case when checking strings
*/
expect()->intercept('toBe', fn ($value) => is_string($value), function ($expected, $ignoreCase = false) {
expect()->intercept('toBe', fn ($value) => is_string($value), function ($expected, $ignoreCase = false): void {
if ($ignoreCase) {
assertEqualsIgnoringCase($expected, $this->value);
} else {
@@ -125,7 +125,7 @@ expect()->intercept('toBe', fn ($value) => is_string($value), function ($expecte
}
});
test('pipe is applied and can stop pipeline', function () use ($state) {
test('pipe is applied and can stop pipeline', function () use ($state): void {
$char = new Char('A');
$state->reset();
@@ -146,7 +146,7 @@ test('pipe is applied and can stop pipeline', function () use ($state) {
]);
});
test('pipe is run and can let the pipeline keep going', function () use ($state) {
test('pipe is run and can let the pipeline keep going', function () use ($state): void {
$state->reset();
expect(3)->toBe(3)
@@ -165,7 +165,7 @@ test('pipe is run and can let the pipeline keep going', function () use ($state)
]);
});
test('pipe works with negated expectation', function () use ($state) {
test('pipe works with negated expectation', function () use ($state): void {
$char = new Char('A');
$state->reset();
@@ -186,7 +186,7 @@ test('pipe works with negated expectation', function () use ($state) {
]);
});
test('interceptor is applied', function () use ($state) {
test('interceptor is applied', function () use ($state): void {
$number = new Number(1);
$state->reset();
@@ -197,7 +197,7 @@ test('interceptor is applied', function () use ($state) {
->appliedCount->toHaveKey('number', 1);
});
test('interceptor stops the pipeline', function () use ($state) {
test('interceptor stops the pipeline', function () use ($state): void {
$number = new Number(1);
$state->reset();
@@ -218,7 +218,7 @@ test('interceptor stops the pipeline', function () use ($state) {
]);
});
test('interceptor is called only when filter is met', function () use ($state) {
test('interceptor is called only when filter is met', function () use ($state): void {
$state->reset();
expect(1)->toBe(1)
@@ -227,7 +227,7 @@ test('interceptor is called only when filter is met', function () use ($state) {
->appliedCount->toHaveKey('number', 0);
});
test('interceptor can be filtered with a closure', function () use ($state) {
test('interceptor can be filtered with a closure', function () use ($state): void {
$state->reset();
expect('*')->toBe(1)
@@ -236,7 +236,7 @@ test('interceptor can be filtered with a closure', function () use ($state) {
->appliedCount->toHaveKey('wildcard', 1);
});
test('interceptor can be filter the expected parameter as well', function () use ($state) {
test('interceptor can be filter the expected parameter as well', function () use ($state): void {
$state->reset();
expect('*')->toBe('*')
@@ -245,13 +245,13 @@ test('interceptor can be filter the expected parameter as well', function () use
->appliedCount->toHaveKey('wildcard', 0);
});
test('interceptor works with negated expectation', function () {
test('interceptor works with negated expectation', function (): void {
$char = new Number(1);
expect($char)->not->toBe(new Char('B'));
});
test('intercept can add new parameters to the expectation', function () {
test('intercept can add new parameters to the expectation', function (): void {
$ignoreCase = true;
expect('Foo')->toBe('foo', $ignoreCase);
+4 -2
View File
@@ -1,5 +1,7 @@
<?php
test('ray calls do not fail when ray is not installed', function () {
expect(true)->ray()->toBe(true);
declare(strict_types=1);
test('ray calls do not fail when ray is not installed', function (): void {
expect(true)->toBeTrue();
});

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