Compare commits

...

711 Commits

Author SHA1 Message Date
nuno maduro 375b92e93b chore: updates sponsors 2026-08-10 11:25:58 +01:00
nuno maduro e0e00ca748 chore: style 2026-08-10 11:12:08 +01:00
nuno maduro d70ecb873e chore: skip --tia on --mutate 2026-08-10 11:07:46 +01:00
nuno maduro 638932ddb5 chore: updates snapshots 2026-08-10 10:22:36 +01:00
nuno maduro ac48185736 chore: upgrades to phpunit:13.3 2026-08-10 10:16:30 +01:00
nuno maduro 2e4f8ad56f chore: fixes snapshots 2026-08-07 15:43:17 +01:00
nuno maduro 8c554a81e2 chore: refactors exceptions 2026-08-07 15:26:55 +01:00
nuno maduro 42d9b777bf chore: coding style changes 2026-08-07 14:52:56 +01:00
nuno maduro eb88513baf fix: livewire 2026-08-07 14:27:26 +01:00
nuno maduro 7b6415ccd0 fix 2026-08-07 03:15:22 +01:00
nuno maduro a5d726b678 release: 5.0.4 2026-08-07 03:00:25 +01:00
nuno maduro 93223caa56 chore: fixes ci 2026-08-07 03:00:19 +01:00
nuno maduro d0572ce138 wip 2026-08-07 02:26:45 +01:00
nuno maduro b0d1fc589d refactor 2026-08-07 02:12:24 +01:00
nuno maduro 5a4f124199 wip 2026-08-07 02:06:28 +01:00
nuno maduro c3ceb7d0c7 qwd 2026-08-07 02:00:58 +01:00
nuno maduro 16d1208344 wip 2026-08-07 01:58:41 +01:00
nuno maduro 1cebc84c2d wip 2026-08-07 01:55:51 +01:00
nuno maduro 1f79660add wip 2026-08-07 01:55:34 +01:00
nuno maduro 37bdf60a1c wip 2026-08-07 01:41:32 +01:00
nuno maduro cc7acfe485 wdq 2026-08-07 01:11:53 +01:00
nuno maduro 7bfb2a6185 chore: upgrade snapshots 2026-08-06 22:24:26 +01:00
nuno maduro ca56ca5216 dw 2026-08-06 22:19:47 +01:00
nuno maduro b49ba062d5 wq 2026-08-06 22:05:15 +01:00
nuno maduro 09699847a2 wqd 2026-08-06 20:53:09 +01:00
nuno maduro 545d0c2784 wip 2026-08-06 20:48:22 +01:00
nuno maduro b795af3b1b wip 2026-08-06 17:52:04 +01:00
nuno maduro a40cc6bc0e more tests 2026-08-06 16:35:03 +01:00
nuno maduro 4d3d0105b7 chore: style 2026-08-06 16:02:36 +01:00
nuno maduro e90e4f70fc wip 2026-08-05 20:20:35 +01:00
nuno maduro d112582857 wip 2026-08-05 19:58:57 +01:00
nuno maduro 9f3c4e1e82 fix 2026-08-05 18:51:49 +01:00
nuno maduro 71f39366c2 wip 2026-08-05 18:21:34 +01:00
nuno maduro bfd5b75677 fix 2026-08-05 16:49:35 +01:00
nuno maduro 411b9954b5 wip 2026-08-05 16:18:03 +01:00
nuno maduro db70017cb2 wip 2026-08-05 03:30:50 +01:00
nuno maduro 86adaedbbd fix: tia filtered 2026-08-04 23:30:03 +01:00
nuno maduro 92c7677c6e fix: don't allow invalid class names 2026-08-04 20:46:21 +01:00
nuno maduro 872f0a50c2 fix(tia): replayable scenarios running afterEach 2026-08-04 20:19:12 +01:00
nuno maduro 668809bc75 chore: adds missing test 2026-08-04 19:28:58 +01:00
nuno maduro 8d8f45c843 chore: bump deps 2026-08-04 19:28:52 +01:00
nuno maduro 086b3e9107 chore: ai instructions 2026-08-04 19:01:16 +01:00
nuno maduro 43fe26f324 chore: bumps deps 2026-08-04 19:01:08 +01:00
nuno maduro 865c1e5113 ci: snapshots 2026-08-04 18:51:04 +01:00
stephanbouman 19eed8d581 Fixed typos (#1819)
* Fixed typo in message

* Fixed typo in message
2026-08-03 11:29:03 -04:00
Danny de Wit ad1850b110 fix: keep TableExtractor table names as strings (#1794)
PHP coerces numeric-string array keys to ints, so collecting table names in
$tables[$name] and returning array_keys() can yield ints. That breaks the
declared list<string> and throws a TypeError in Recorder::linkTable(string).

Standard SQL like substring(x FROM 1 FOR 10) is enough to trigger it, since
the numeric operand matches the FROM pattern.

Closes #1793

Co-authored-by: Danny de Wit <d.dewit@fenceweb.com>
2026-08-02 22:50:47 -04:00
nuno maduro 585de259a2 chore: style 2026-08-02 22:26:34 -04:00
nuno maduro 953664dce0 release: v5.0.3 2026-08-02 22:24:32 -04:00
nuno maduro 1dd959848c chore: updates snapshots 2026-08-02 22:24:24 -04:00
Jeffrey van Hees 047753c836 fix: key Tia results per dataset row (#1799)
The result cache was keyed `Class::method` on both sides, so every row of a
`->with()` test shared one entry and the last writer won. On replay that single
status was handed to every row.

`TestMethod::id()` already appends `#dataSetName`, and `TestCase` reaches the
same value object through `valueObjectForEvents()`, so both sides can use it.
2026-08-02 21:43:15 -04:00
nuno maduro b63626a94d docs: adjusts readme
Signed-off-by: nuno maduro <enunomaduro@gmail.com>
2026-07-30 11:53:41 -04:00
nuno maduro ae080becd6 chore: style 2026-07-29 15:27:11 -04:00
nuno maduro aa86304794 release: v5.0.2 2026-07-29 14:59:13 -04:00
nuno maduro 75963ea973 chore: bumps deps 2026-07-29 14:59:03 -04:00
ohnotnow 5d0ceb1a5d Check for pcov being enabled for tia (#1779)
* Check for pcov being enabled for tia

* Delete tests/Features/TiaDriverGate.php

Signed-off-by: nuno maduro <enunomaduro@gmail.com>

---------

Signed-off-by: nuno maduro <enunomaduro@gmail.com>
Co-authored-by: Ohffs <ohffsnotnow@gmail.com>
Co-authored-by: nuno maduro <enunomaduro@gmail.com>
2026-07-29 14:28:38 -04:00
talaridisTh 6f0c0536a4 feat: document the tia options in pest --help (#1780) 2026-07-29 14:11:17 -04:00
Pushpak Chhajed 6b2cd358e8 Add Laravel Boost skill for Pest 5 testing (#1775) 2026-07-28 13:28:46 -04:00
Than Tibbetts e09e1be76d Update Pest v5 announcement link in README (#1773)
Signed-off-by: Than Tibbetts <than@me.com>
2026-07-28 12:21:33 -04:00
dependabot[bot] 759ac63139 build(deps): bump actions/checkout in the github-actions group (#1768)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 7.0.0 to 7.0.1
- [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/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.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-07-24 01:48:21 +01:00
nuno maduro 333f469efb adjustments
Signed-off-by: nuno maduro <enunomaduro@gmail.com>
2026-07-23 00:25:58 +01:00
nuno maduro c4ec925aa5 Revise Pest description in README.md
Updated the description of Pest to include AI agents.

Signed-off-by: nuno maduro <enunomaduro@gmail.com>
2026-07-22 22:15:30 +01:00
nuno maduro 5c8cfed483 docs: sync sponsors with pestphp.com
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:58:54 +01:00
nuno maduro 962510b7e3 chore: adjusts snapshots 2026-07-21 01:14:59 +01:00
Punyapal Shah e9c268a584 feat: validation expectations (#1762)
* feat: add validation expectations for IP, MAC, hostname, domain, base64, and hexadecimal

* improvements

* improvements
2026-07-20 11:21:57 +01:00
nuno maduro fa91fcf744 fix: coverage merger 2026-07-19 16:45:06 +01:00
nuno maduro c6073511a2 chore: fixes test printer 2026-07-19 13:59:31 +01:00
nuno maduro b5e1b83bb4 fix: cache results on parallel 2026-07-19 12:29:33 +01:00
nuno maduro 1c11595504 chore: moves to coding stlye 2026-07-19 03:41:57 +01:00
nuno maduro cd0e921158 chore: style 2026-07-19 00:42:29 +01:00
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 40b88b62ef release: v4.7.2 2026-06-01 07:08:59 +01:00
nuno maduro e3361bc321 fix: don't ob_start with pao 2026-06-01 07:08:50 +01:00
nuno maduro fc48c1bd1e Merge branch '4.x' into 5.x 2026-06-01 06:33:35 +01:00
nuno maduro 92e76eb5ab ci: runs ci only against stable 2026-06-01 06:32:29 +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 bd22f478b8 chore: fixes issues with contracts and symfony 8.1 2026-06-01 06:24:42 +01:00
nuno maduro eeaac34cf6 release: v4.7.1 2026-06-01 05:42:12 +01:00
nuno maduro b9b07d8983 chore: bump dependencies 2026-06-01 05:42:03 +01:00
nuno maduro 6aa7d2f891 fix: better fatal exceptions reporting 2026-06-01 05:41:58 +01:00
nuno maduro 4d550cecfd Merge branch '4.x' into 5.x 2026-05-13 12:20:46 +01:00
nuno maduro 1c21a7647a chore: fixes types 2026-05-13 12:20:00 +01:00
nuno maduro d649de1988 chore: add security policy 2026-05-12 02:48:25 +01:00
nuno maduro 783ca4bcd6 chore(deps): limit dependabot to maintained branches (4.x + 5.x) 2026-05-12 02:34:08 +01:00
nuno maduro ba07497219 chore: enable Dependabot version updates for GitHub Actions (#1700) 2026-05-11 22:12:07 -03:00
nuno maduro 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 1ca021dea6 chore: pin GitHub Actions to commit SHAs (#1695)
* chore: pin GitHub Actions to commit SHAs

* chore: pin GitHub Actions to commit SHAs
2026-05-12 02:08:47 +01:00
nuno maduro 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 2fc75cfcf0 chore: updates snapshots 2026-05-03 13:09:32 -03:00
nuno maduro 6cc48f63f8 chore: style 2026-05-03 13:06:24 -03:00
nuno maduro e0419d1328 release: v4.7.0 2026-05-03 12:46:24 -03:00
nuno maduro faa6988801 Merge pull request #1682 from pestphp/feat/tia
[4.x] TIA Engine
2026-05-03 16:27:58 +01:00
nuno maduro c12247fafd Revert "wip"
This reverts commit 1b168aba1c.
2026-05-03 11:45:39 -03:00
nuno maduro 29b4452443 wip 2026-05-03 11:33:30 -03:00
nuno maduro 1b168aba1c wip 2026-05-03 11:30:13 -03:00
nuno maduro 6aabd977cd wip 2026-05-03 11:01:52 -03:00
nuno maduro a882543c53 wip 2026-05-03 10:57:28 -03:00
nuno maduro c250b9da4f wip 2026-05-03 10:37:17 -03:00
nuno maduro 46bc3dc628 wip 2026-05-03 10:34:44 -03:00
nuno maduro d3ce498b8a wip 2026-05-03 10:31:47 -03:00
nuno maduro e1a4b98b71 wip 2026-05-03 10:16:10 -03:00
nuno maduro 9afbcd5c18 wuip 2026-05-03 09:54:02 -03:00
nuno maduro 75593b6454 wip 2026-05-03 13:37:27 +01:00
nuno maduro 89590d6120 wip 2026-05-03 13:35:01 +01:00
nuno maduro fb0978c9bf wip 2026-05-03 13:26:48 +01:00
nuno maduro a3796daa42 wip 2026-05-03 13:24:10 +01:00
nuno maduro e3004db666 wip 2026-05-03 12:43:38 +01:00
nuno maduro 99cc4e0146 wip 2026-05-02 19:33:09 +01:00
nuno maduro a47e6f8fef wip 2026-05-02 19:30:14 +01:00
nuno maduro 536d79f765 wip 2026-05-02 19:20:55 +01:00
nuno maduro 65c0fbc528 wip 2026-05-02 19:07:41 +01:00
nuno maduro 9e4cf4b665 wip 2026-05-02 18:58:42 +01:00
nuno maduro 7bea819978 wip 2026-05-02 18:47:26 +01:00
nuno maduro 4280233b40 wip 2026-05-02 18:37:24 +01:00
nuno maduro d6db3a8a20 wip 2026-05-02 18:32:05 +01:00
nuno maduro 51c8ce4df6 wip 2026-05-02 18:31:32 +01:00
nuno maduro 5b8393b925 wip 2026-05-02 18:25:41 +01:00
nuno maduro e4d9b61fdf wip 2026-05-02 18:25:27 +01:00
nuno maduro e2d940cd53 wip 2026-05-02 18:25:21 +01:00
nuno maduro 380ccd30b4 wip 2026-05-02 18:03:25 +01:00
nuno maduro 31c200716d wip 2026-05-02 18:03:14 +01:00
nuno maduro 6add4da543 wip 2026-05-02 18:02:20 +01:00
nuno maduro 8ddcd3e853 wip 2026-05-02 18:02:13 +01:00
nuno maduro e3e178fd94 wip 2026-05-02 17:59:21 +01:00
nuno maduro 7b1ec9f003 wip 2026-05-02 17:59:13 +01:00
nuno maduro 1e48c5d473 wip 2026-05-02 17:59:00 +01:00
nuno maduro d00ec95dd9 wip 2026-05-02 17:58:55 +01:00
nuno maduro 89f3d6cb39 wip 2026-05-02 17:45:54 +01:00
nuno maduro a07a2e512a wip 2026-05-02 17:39:15 +01:00
nuno maduro 57eecb2b3d wip 2026-05-02 17:38:12 +01:00
nuno maduro 9f804dc954 wip 2026-05-02 17:38:08 +01:00
nuno maduro 7cbad4c589 wip 2026-05-02 17:38:01 +01:00
nuno maduro 5cae93b059 wip 2026-05-02 17:37:56 +01:00
nuno maduro df829ad19d wip 2026-05-02 17:37:47 +01:00
nuno maduro 635460653c wip 2026-05-02 17:37:34 +01:00
nuno maduro 1aa80dc398 wip 2026-05-02 17:18:35 +01:00
nuno maduro 8a14056111 wip 2026-05-02 17:15:46 +01:00
nuno maduro f247dd8e7b wip 2026-05-02 17:11:49 +01:00
nuno maduro 1c7c9754fd wip 2026-05-02 17:07:08 +01:00
nuno maduro 5f37939fda wip 2026-05-02 17:02:11 +01:00
nuno maduro 28305fcb7a wip 2026-05-02 16:35:52 +01:00
nuno maduro 5242803694 wip 2026-05-02 15:54:00 +01:00
nuno maduro 925935a7e8 wip 2026-05-02 15:33:38 +01:00
nuno maduro 460401c379 wip 2026-05-02 15:26:58 +01:00
nuno maduro 348b439172 wip 2026-05-02 15:15:53 +01:00
nuno maduro a4e77766c5 wip 2026-05-02 15:07:51 +01:00
nuno maduro 4a8c2d7d78 wip 2026-05-02 15:03:44 +01:00
nuno maduro 7d51601120 wip 2026-05-02 14:15:37 +01:00
nuno maduro 631bbe318b wip 2026-05-02 13:43:32 +01:00
nuno maduro 9b7c15d5b6 wip 2026-05-02 12:03:35 +01:00
nuno maduro 872796bd9b wip 2026-05-02 12:00:47 +01:00
nuno maduro c38d32ae86 wip 2026-05-02 09:49:33 +01:00
nuno maduro 6407c4f78f wip 2026-05-02 01:58:39 +01:00
nuno maduro 6e1bf63f6a wip 2026-05-02 01:40:35 +01:00
nuno maduro 1d3e8bb5dd wip 2026-05-02 01:03:06 +01:00
nuno maduro 3cc9b169e3 wip 2026-05-02 00:52:57 +01:00
nuno maduro c4911d046b wip 2026-05-02 00:06:04 +01:00
nuno maduro d0295f6168 wip 2026-05-01 23:59:25 +01:00
nuno maduro 21efbc3107 wip 2026-05-01 22:55:38 +01:00
nuno maduro e59b99cd73 wip 2026-05-01 22:51:55 +01:00
nuno maduro bf48e20880 wip 2026-05-01 22:36:15 +01:00
nuno maduro 53db68e005 wip 2026-05-01 22:31:00 +01:00
nuno maduro 34f1e9a7f2 fix 2026-05-01 21:51:09 +01:00
nuno maduro 57fd5ce042 wip 2026-05-01 21:50:56 +01:00
nuno maduro 3bcabfb63b fix 2026-05-01 21:50:52 +01:00
nuno maduro aa3a7c303a wip 2026-05-01 21:32:59 +01:00
nuno maduro 5c08a135f7 wip 2026-05-01 21:30:44 +01:00
nuno maduro 6e0e030d71 wip 2026-05-01 21:22:33 +01:00
nuno maduro b2c07561e7 wip 2026-05-01 20:54:24 +01:00
nuno maduro 97600b6f0b wip 2026-05-01 20:53:40 +01:00
nuno maduro 8a51f15d65 wip 2026-05-01 20:45:51 +01:00
nuno maduro a349f53964 wip 2026-05-01 20:42:14 +01:00
nuno maduro a725e774c0 wip 2026-05-01 20:28:39 +01:00
nuno maduro bed5e5b54a wip 2026-05-01 20:02:46 +01:00
nuno maduro 45b1d4ce20 wip 2026-05-01 19:50:54 +01:00
nuno maduro d106b70766 wip 2026-05-01 17:24:22 +01:00
nuno maduro 6ac6c1518e wip 2026-05-01 17:17:33 +01:00
nuno maduro fda515a17f wip 2026-05-01 16:42:01 +01:00
nuno maduro 0a97d3a288 asd 2026-05-01 15:33:06 +01:00
nuno maduro 3802fa80e6 asd 2026-05-01 15:19:19 +01:00
nuno maduro 5c3cbc14d2 wip 2026-05-01 15:07:10 +01:00
nuno maduro 6b9c768172 wip 2026-05-01 14:39:23 +01:00
nuno maduro 4a2fc179ae asd 2026-05-01 13:54:25 +01:00
nuno maduro b5bb2139dc wqdqwd 2026-05-01 12:57:12 +01:00
nuno maduro 07416a3c61 wip 2026-05-01 03:30:28 +01:00
nuno maduro 30b94e3034 qdw 2026-05-01 02:10:08 +01:00
nuno maduro be34eecb2f wip 2026-05-01 01:55:18 +01:00
nuno maduro 5d9f95f8d4 qwdqwd 2026-05-01 01:44:08 +01:00
nuno maduro 48b70a03d5 wip 2026-05-01 01:32:48 +01:00
nuno maduro 4b8642b972 wip 2026-05-01 00:48:31 +01:00
nuno maduro 8711d51eac fix 2026-05-01 00:19:44 +01:00
nuno maduro 58dfb6da64 wip 2026-04-30 22:12:53 +01:00
nuno maduro d7735d1faa wip 2026-04-30 22:00:56 +01:00
nuno maduro 6b59166f3c wip 2026-04-30 21:08:00 +01:00
nuno maduro 3a26028d17 wip 2026-04-30 20:58:06 +01:00
nuno maduro 3c91bf4ad2 wip 2026-04-30 20:51:57 +01:00
nuno maduro 6a434be0f6 wip 2026-04-30 20:45:36 +01:00
nuno maduro f355b99bbf wip 2026-04-29 22:59:56 +01:00
nuno maduro 95a00341e9 wip for now 2026-04-29 02:22:37 +01:00
nuno maduro 466259646d wip 2026-04-28 22:12:42 +01:00
nuno maduro 00f8d56083 wip 2026-04-28 21:41:20 +01:00
nuno maduro ca2dca592d wup 2026-04-28 21:34:40 +01:00
nuno maduro 405d8d4406 wip 2026-04-28 21:28:46 +01:00
nuno maduro b944ee5841 wip 2026-04-27 19:15:42 +01:00
nuno maduro f4e22dcafe wip 2026-04-27 18:57:41 +01:00
nuno maduro 339c1e8cac wip 2026-04-27 18:14:10 +01:00
nuno maduro d4c7362132 wip 2026-04-27 16:56:27 +01:00
nuno maduro 81bfdbf8fe wip 2026-04-27 13:16:05 +01:00
nuno maduro f45cbf43c5 wip 2026-04-27 13:11:48 +01:00
nuno maduro b9088d23fb wip 2026-04-27 13:03:07 +01:00
nuno maduro 7250185423 wip 2026-04-27 12:22:05 +01:00
nuno maduro e457eb0e9c wip 2026-04-27 11:15:59 +01:00
nuno maduro 48357c6f30 wip 2026-04-27 10:30:08 +01:00
nuno maduro b46f051550 wip 2026-04-23 17:32:27 -07:00
nuno maduro 3d3c5d41ac wip 2026-04-23 12:29:24 -07:00
nuno maduro caabebf2a1 wip 2026-04-23 10:56:17 -07:00
nuno maduro 470a5833d4 wip 2026-04-23 10:30:44 -07:00
nuno maduro c1feefbb9e wip 2026-04-23 09:44:12 -07:00
nuno maduro e876dba8ba wip 2026-04-23 09:29:56 -07:00
nuno maduro d9c18f9c02 wip 2026-04-22 09:03:10 -07:00
nuno maduro 660b57b365 wip 2026-04-22 08:42:32 -07:00
nuno maduro 68527c996f wip 2026-04-22 08:25:38 -07:00
nuno maduro c6a42a2b28 wip 2026-04-22 08:07:52 -07:00
nuno maduro fcf5c27914 chore: adds YouTube channel badge
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:56:04 -07:00
nuno maduro 856a370032 style 2026-04-21 09:44:26 -07:00
nuno maduro e24882c486 wip 2026-04-21 09:41:19 -07:00
nuno maduro 51fc380789 wip 2026-04-21 09:40:01 -07:00
nuno maduro f6609f4039 wip 2026-04-21 08:36:41 -07:00
nuno maduro 2941f9821f wip 2026-04-21 08:15:24 -07:00
nuno maduro ed399af43e wip 2026-04-21 07:41:50 -07:00
nuno maduro 0d66dc4322 chore: removes https 2026-04-21 07:26:19 -07:00
nuno maduro 7e4280bf83 chore: improves feedback 2026-04-21 07:13:08 -07:00
nuno maduro a5915b16ab wip 2026-04-20 20:58:38 -07:00
nuno maduro 1476b529a1 wip 2026-04-20 19:56:23 -07:00
nuno maduro 2892341c28 wip 2026-04-20 14:28:18 -07:00
nuno maduro 59e781e77b wip 2026-04-20 13:48:05 -07:00
nuno maduro 55a3394f8c wip 2026-04-20 13:31:43 -07:00
nuno maduro 0d99c33c4e wip 2026-04-20 13:16:59 -07:00
nuno maduro adc5aae6f8 wip 2026-04-20 13:00:41 -07:00
nuno maduro 980667e845 wip 2026-04-20 12:25:39 -07:00
nuno maduro 8c849c5f40 fix: missing warning 2026-04-20 12:18:32 -07:00
nuno maduro 47f1fc2d94 feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro 9c8033d60c feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro 42d1092a9e feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro c7e32f5d33 feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro d379128cc4 feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro f09d6f2064 feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro 494cc6e2a4 feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro f52a455773 fix 2026-04-20 10:09:07 -07:00
nuno maduro 184f5d2742 feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro 1d81069a2a feat(tia): continues to work on poc 2026-04-20 10:09:07 -07:00
nuno maduro 4b9bb77b54 feat(tia): adds poc 2026-04-20 10:09:07 -07:00
nuno maduro c440031e28 feat(tia): adds poc 2026-04-20 10:09:07 -07:00
nuno maduro 18bbca748f Merge branch '4.x' into 5.x 2026-04-18 07:03:46 -07:00
nuno maduro bff44562a9 release: v4.6.3 2026-04-18 06:51:25 -07:00
nuno maduro 9ebb990f96 chore: bumps phpunit 2026-04-18 06:51:17 -07:00
nuno maduro f142aad8ad Merge branch '4.x' into 5.x 2026-04-17 19:35:53 -07:00
nuno maduro cabff738f7 release: v4.6.2 2026-04-17 19:32:23 -07:00
nuno maduro 0746173a32 chore: bumps phpunit 2026-04-17 19:32:18 -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 87db0b4847 release: v4.6.1 2026-04-15 09:03:09 -07:00
nuno maduro 6ba373a772 chore: bumps phpunit 2026-04-15 08:49:34 -07:00
nuno maduro 945d476409 fix: allow to update individual screenshots 2026-04-15 08:34:06 -07:00
nuno maduro a8cf0fe2cb chore: improves CI 2026-04-15 08:20:50 -07:00
nuno maduro 2ae072bb95 feat: makes boot time much faster 2026-04-15 07:47:38 -07:00
nuno maduro 59d066950c chore: missing header 2026-04-15 07:47:22 -07:00
nuno maduro 0dd1aa72ef fix: updating snapshots in --parallel 2026-04-15 07:22:10 -07:00
nuno maduro 4e03cd3edb release: v4.6.0 2026-04-14 10:23:26 -07:00
nuno maduro eeab24e2bb Merge pull request #1671 from pestphp/feat/time-based-sharding
[4.x] Time based sharding
2026-04-14 18:18:09 +01:00
nuno maduro 9b64d5425a removes time balanced 2026-04-14 10:12:57 -07:00
nuno maduro 0acab1cbb4 wip 2026-04-14 09:53:57 -07:00
nuno maduro e616eab9fb wip 2026-04-14 09:36:38 -07:00
nuno maduro 7cbb1fcdb2 wip 2026-04-14 09:29:41 -07:00
nuno maduro cb5f6e1bd2 chore: style 2026-04-14 09:17:18 -07:00
nuno maduro 985dadd934 update 2026-04-14 09:16:32 -07:00
nuno maduro 10aee6045c feat(time-based-sharding): updates exception name 2026-04-14 09:08:52 -07:00
nuno maduro 4ac14b2528 feat(time-based-sharding): updates plugin 2026-04-14 08:34:41 -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 13c322bab3 ci: fixes incorrectCasing test 2026-04-10 20:51:40 +01:00
nuno maduro 3855249ce9 feat: adds --flaky cli option 2026-04-10 20:03:50 +01:00
nuno maduro f528bd8427 feat: adds flaky 2026-04-10 19:52:31 +01:00
nuno maduro acd8aafa63 fix: printer with --colors 2026-04-10 19:21:49 +01:00
nuno maduro e8d630e774 fix: printer with --colors 2026-04-10 19:21:41 +01:00
nuno maduro b6385dc865 fix: namespaces on toBeCasedCorrectly 2026-04-10 19:21:31 +01:00
nuno maduro 02dc8d7bcc chore: bumps deps 2026-04-10 19:21:18 +01:00
nuno maduro 729f18a152 fix: stacktrace with nested with calls 2026-04-10 17:25:05 +01:00
nuno maduro bdf60cea91 Merge pull request #1565 from louisbels/fix-dataset-method-chaining
fix: dataset inheritance with method chaining (beforeEach()->with(), describe()->with())
2026-04-10 17:05:25 +01:00
nuno maduro 3a8ee8291c Merge pull request #1628 from DevDavido/patch-1
fix: Parameter closure this type annotations in Functions
2026-04-10 16:58:39 +01:00
nuno maduro 654cb726c9 Merge branch '4.x' into patch-1 2026-04-10 16:58:26 +01:00
nuno maduro bce26aeaad Merge pull request #1634 from dbpolito/dataset_named_params
Dataset Named Parameters
2026-04-10 16:54:57 +01:00
nuno maduro 5948bcd71e chore: type improvements 2026-04-10 16:50:10 +01:00
nuno maduro 89006d83a9 chore: env 2026-04-10 16:27:44 +01:00
nuno maduro a8e974d64a chore: updates snapshots 2026-04-10 14:42:34 +01:00
nuno maduro 617b074049 Merge pull request #1626 from SimonBroekaert/feat/add_only-covered_option
feat: add '--only-covered' option to '--coverage'
2026-04-10 12:53:17 +01:00
nuno maduro 2eea71a664 Merge pull request #1624 from Vmadmax/fix/unicode-filename-filter
fix: preserve Unicode characters in filenames for --filter matching
2026-04-10 12:29:40 +01:00
nuno maduro 4b5374d507 Merge branch '4.x' into fix/unicode-filename-filter 2026-04-10 12:29:30 +01:00
nuno maduro 9085561ece chore: runs at 9am 2026-04-10 12:24:30 +01:00
nuno maduro b71bfc513a chore: guards 2026-04-10 12:23:49 +01:00
nuno maduro 75938ac9eb ci: updates deps 2026-04-10 12:18:28 +01:00
nuno maduro e766825f5b chore: fixes test:unit 2026-04-10 12:15:00 +01:00
nuno maduro 8a83a1a1a9 Merge pull request #1655 from stsepelin/fix/parallel-empty-suite-reporting
fix: nested dataset discovery and parallel invalid-dataset reporting
2026-04-10 11:59:48 +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 109bb22c5e Merge pull request #1615 from smirok/parallel-teamcity-concurrency-fix
fix: enhance support for --parallel and --teamcity arguments by restoring --teamcity for ParaTest and fixing teamcity output concurrency
2026-04-10 11:42:45 +01:00
nuno maduro 89dd212d84 Merge pull request #1580 from bibrokhim/patch-1
Add Rules to Laravel preset
2026-04-10 11:42:13 +01:00
nuno maduro cd07c6d966 Merge pull request #1569 from treyssatvincent/patch-1
add missing classes before toExtend on laravel preset
2026-04-10 11:41:53 +01:00
nuno maduro 8dddb47ad5 Merge branch '4.x' into fix-dataset-method-chaining 2026-04-10 11:41:13 +01:00
nuno maduro 3a6c2fab37 Merge pull request #1515 from yondifon/fix-trait-inheritance-detection
BugFix: Fix toUseTrait to detect inherited and nested traits
2026-04-10 11:39:56 +01:00
nuno maduro 281dbf6cf4 Merge pull request #1455 from SimonBroekaert/feat/to_be_cased_correctly_arch_test_assertion
feat: add toBeCasedCorrectly arch test assertion
2026-04-10 11:38:46 +01:00
nuno maduro 40c8429058 Merge pull request #1653 from orphanedrecord/fix/pest-php-stub-typo
Fix typo in Pest.php stubs
2026-04-10 11:35:41 +01:00
nuno maduro 8dd650fd05 Merge branch '4.x' into 5.x 2026-04-09 21:39:15 +01:00
nuno maduro d9d46c73f8 chore: stores statically the result 2026-04-09 21:36:49 +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 e44c554a0b chore: bumps dependencies 2026-04-06 21:57:05 +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 9797a71dbc feat(ai): allow temporary namesapce 2026-04-03 14:43:28 +01:00
nuno maduro c1a54df233 feat: --ai work in progress 2026-04-03 14:04:20 +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 ce05ee9aad release: v4.4.4 2026-04-03 12:00:04 +01:00
nuno maduro 3d2ebdb273 bump: dependencies 2026-04-03 11:59:54 +01:00
nuno maduro 668ca9f5de feat: adds pao 2026-04-02 15:45:13 +01:00
nuno maduro f47b74445b Merge pull request #1657 from morpheus7CS/pint-formatting-rules-applied-to-stubs 2026-04-02 13:04:00 +01:00
Gal Jakic (We Wow Web) 6c42e7f4ea Laravel Pint default formatting applied to Pest-php.stub 2026-04-01 16:48:14 +02:00
dbpolito be3ff37517 Merge branch '4.x' of https://github.com/pestphp/pest into dataset_named_params 2026-03-26 18:08:26 -03:00
nuno maduro a087555383 bump: dependencies 2026-03-26 14:30:03 +00:00
Aleksandr Štšepelin 4b50cb486d Restore success snapshot coverage with lower memory limit 2026-03-25 23:59:29 +02:00
Aleksandr Štšepelin f7175ecfd7 Fix parallel dataset reporting and nested fixtures 2026-03-25 23:59:29 +02:00
Aleksandr Štšepelin 07737bc0b2 Fix parallel file selection and empty-suite reporting 2026-03-25 23:59:28 +02:00
nuno maduro f659a45311 Merge branch '4.x' into 5.x 2026-03-21 13:20:25 +00:00
nuno maduro e6ab897594 release: v4.4.3 2026-03-21 13:14:39 +00:00
nuno maduro a753b41409 chore: bumps phpunit 2026-03-21 13:14:35 +00:00
brianseymour 1a4c06bd6e Fix Pest comment typo while still honoring the Otwellian Waterfall 2026-03-18 20:51:45 -07:00
nuno maduro 12c1da29ee Merge branch '4.x' into 5.x 2026-03-10 21:21:24 +00:00
nuno maduro 5d42e8fe3a release: v4.4.2 2026-03-10 21:09:12 +00:00
nuno maduro 9d17b872dd chore: update stubs 2026-03-10 21:09:02 +00:00
nuno maduro 2a80101f42 chore: update styling 2026-03-10 21:06:28 +00:00
Nuno Maduro f7015fe59c chore: adjusts sponsors 2026-02-24 10:44:48 +00:00
nuno maduro 7281e0ded7 Add SerpApi to the list of sponsors
Add SerpApi as a sponsor in the README.
2026-02-20 01:18:43 +00:00
SimonBroekaert 1675dd1d41 chore: add tests for toBeCasedCorrectly() arch test 2026-02-17 19:03:46 +01: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
SimonBroekaert df7b6c8454 feat: add toBeCasedCorrectly arch test assertion 2026-02-17 17:31:02 +01:00
Nuno Maduro 5de8693e3b chore: style 2026-02-17 16:15:58 +00:00
Nuno Maduro 7d80f1d20e chore: removes non used files 2026-02-17 15:34:32 +00:00
nuno maduro b3119cc120 Merge pull request #1562 from michaelw85/patch-1
[Fix] Pass test dir to worker
2026-02-17 15:33:16 +00:00
nuno maduro 4e294edf76 Merge pull request #1639 from imliam/patch-1
[Laravel Preset] Allow App\Http to be used in providers
2026-02-17 15:31:01 +00:00
Nuno Maduro f96a1b2786 release: v4.4.1 2026-02-17 15:27:18 +00:00
Nuno Maduro a49cf7edc5 ci: speed up ci 2026-02-17 15:21:20 +00:00
Nuno Maduro b0f6a74cb6 ci: makes jobs faster 2026-02-17 15:18:33 +00:00
Nuno Maduro aaa226f6a6 chore: tests against symfony 8 2026-02-17 15:14:45 +00:00
Nuno Maduro 69cb752d02 chore: bumps dependencies 2026-02-17 15:01:37 +00:00
Nuno Maduro cf00e58b7d chore: bumps dependencies 2026-02-17 11:22:04 +00:00
Liam Hammett 1f39b28e2c Allow App\Http to be used in providers 2026-02-16 00:25:47 +01:00
nuno maduro 9fcbca69d4 Update README.md 2026-02-13 10:41:22 +00:00
dbpolito b081584ab6 Improvements 2026-02-11 18:09:09 -03:00
dbpolito 6966802afc Cleanup 2026-02-11 18:02:21 -03:00
dbpolito c61dcad42b Dataset Named Parameters 2026-02-11 17:57:07 -03:00
David B. ec3e0b2d33 fix: Parameter closure this type annotations in Functions.php 2026-02-09 20:48:56 +01:00
SimonBroekaert c3620840b4 feat: add '--only-covered' option to '--coverage' 2026-02-06 11:57:21 +01:00
vmadmax 10a19f16ba refactor: simplify regex to use Unicode properties \p{L} and \p{N 2026-02-02 09:41:54 +01:00
vmadmax a956de5446 fix: preserve Unicode characters in class names for --filter matching 2026-02-02 09:01:24 +01:00
Nuno Maduro 3a4329ddc7 release: 4.3.2 2026-01-28 01:01:19 +00:00
Ilia Smirnov e6f511302b fix: enhance support for --parallel and --teamcity arguments by restoring --teamcity for ParaTest and fixing teamcity output concurrency 2026-01-27 16:47:24 +01:00
nuno maduro dd01229d7b Merge pull request #1606 from smirok/teamcity-fix-for-describe-tests-with-dataset
fix: replace `substr` with `mb_substr` in Str::beforeLast to ensure multibyte string compatibility and correct TeamCity test names for datasets in "describe" blocks
2026-01-15 01:52:01 +00:00
Ilia Smirnov c7e4efcea4 fix: replace substr with mb_substr in Str::beforeLast to ensure multibyte string compatibility and correct TeamCity test names for datasets in "describe" blocks 2026-01-14 00:55:35 +01:00
nuno maduro df3205e814 Merge pull request #1554 from pindab0ter/feature/extend-closure-this
Specify closure this for extend
2026-01-13 01:19:47 +00:00
Nuno Maduro bc57a84e77 release: 4.3.1 2026-01-04 11:29:59 -05:00
nuno maduro bc39830d8a chore: removes toHaveSuspiciousCharacters from php preset 2026-01-04 11:25:57 -05:00
nuno maduro 3a566b100e docs: why php 2026-01-04 11:04:03 -05:00
nuno maduro 9fe61e0e56 docs: update sponsors
Removed and updated sponsor links in the README.
2026-01-03 18:07:02 -05:00
Nuno Maduro e86bec3e68 release: 4.3.0 2025-12-30 14:48:33 -05:00
nuno maduro 58b8f3cc5d Merge pull request #1598 from Willem-Jaap/willem-jaap/pest-only-file-level
feat: add pest only function to mark each test in a file as only
2025-12-30 14:40:04 -05:00
Willem-Jaap c157b661f2 style: format 2025-12-30 09:26:35 +01:00
Willem-Jaap be90610f17 feat: add pest only function to mark each test in a file as only 2025-12-30 09:24:05 +01:00
nuno maduro 1701a306c3 Merge pull request #1590 from pestphp/feature/intl-exception
feat: show more useful exception when `intl` extension not found
2025-12-29 22:09:16 -05:00
nuno maduro 064ab3fc2e Merge pull request #1595 from bilboque/dirty-flag-not-found-in-help
feat: add --dirty documentation in --help
2025-12-29 22:08:12 -05:00
leo 44e315df98 feat: add --dirty documentation in --help 2025-12-22 11:02:28 +01:00
Nuno Maduro 62694c14b9 chore: style 2025-12-15 11:54:24 +00:00
nuno maduro 7c43c1c583 Merge pull request #1586 from jackbayliss/bump-checkout-action
[4.x] Bump checkout version from 5 to 6
2025-12-15 11:49:28 +00:00
Nuno Maduro 6a96aed654 feat: adds phpunit@12.5 support 2025-12-15 11:48:43 +00:00
Owen Voke b1c997a869 feat: show more useful exception when intl extension not found 2025-12-12 12:02:00 +00:00
Jack Bayliss b4172e2c2e bump checkout version from 5 to 6 2025-12-10 14:08:06 +00:00
Nuno Maduro ae419afd36 chore: support for symfony 8.0.0 components 2025-11-28 12:04:48 +00:00
nuno maduro 27aa305897 Merge pull request #1576 from Chris53897/feature/ci
ci: bump actions/checkout 4 => 5
2025-11-25 11:18:28 +00:00
Bakhromjon 0e7c2abe8b Add Rules to Laravel preset 2025-11-25 15:32:36 +05:00
Nuno Maduro f5820bd670 release: 4.1.5 2025-11-24 12:46:38 +00:00
Nuno Maduro 41fd831153 release: 4.1.4 2025-11-24 10:25:45 +00:00
Christopher Georg 51340439e8 ci: bump actions/checkout 4 => 5 2025-11-22 12:12:15 +01:00
Nuno Maduro 1a39826935 ci: tests against php 8.5 2025-11-20 02:59:27 +00:00
Nino bd5fed9e12 add missing classes before toExtend on laravel preset
Add missing `->classes()` before `->toExtend()` on the laravel preset for 2 namespaces.

Otherwise you can't use interfaces on theses namespace.

For example, if you create an interface `YourSuperRequestContract` on the `app/Http/Requests` namespace you will get this error:

```
Expecting 'app/Http/Requests/YourSuperRequestContract.php' to extend 'Illuminate\Foundation\Http\FormRequest'.
```
2025-11-10 15:26:56 +00:00
Louis 26345fd9f4 fix: dataset inheritance with method chaining (beforeEach()->with(), describe()->with())
Fixes issue where datasets were not applied when using method chaining patterns
like beforeEach()->with([...]) or describe()->with([...]) inside nested describe blocks.

Root cause: Multiple functions were using Backtrace::file() which returns the
immediate caller's filename. This breaks when called through method chaining
because the backtrace returns internal Pest files instead of the test file.

Solution: Use Backtrace::testFile() which walks the entire backtrace to find
the actual test file being executed. This matches the pattern already used by
test() and describe() functions.

Changes in src/Functions.php:
- beforeEach(): Use testFile() to fix beforeEach()->with() pattern
- afterEach(): Use testFile() for consistency with beforeEach()
- beforeAll(): Use testFile() for better error messages
- afterAll(): Use testFile() for better error messages
- pest(): Use testFile() to fix pest()->beforeEach() pattern
- uses(): Use testFile() for consistency with pest()
- covers(): Use testFile() for correct test file context
- mutates(): Use testFile() for correct test file context

Changes in src/PendingCalls/DescribeCall.php:
- __destruct(): Force BeforeEachCall destructor before test creation
- __call(): Use $this->filename instead of Backtrace, more efficient
- __call(): Properly merge describing context for nested describe blocks

Fixes patterns:
- beforeEach()->with([...])
- describe()->with([...])
- pest()->beforeEach()->with([...]

Tests passing:
- tests/Features/Describe.php (all dataset tests)
- tests/Hooks/BeforeEachTest.php (global hook execution)
- tests/Features/Expect/toMatchSnapshot.php (28 tests)
2025-11-05 17:46:52 +01:00
Michael Withagen ae1da79ac1 Pass test dir to worker
#1444 
Test directory argument is lost when spawning workers, add it again.
2025-11-05 17:15:51 +01:00
nuno maduro 00990efc97 Merge pull request #1544 from Se7en-RU/fix-testdox-columns-warning
Fix Undefined array key "testdox-columns" warning
2025-11-04 07:42:57 +00:00
Nuno Maduro 477d20a54f release: 4.1.3 2025-10-29 22:45:27 +00:00
Hans van Luttikhuizen-Ross b7b16096db Specify closure this for extend 2025-10-29 11:20:08 +01:00
Yury Kudryavtsev 4105e33c39 Fix Undefined array key "testdox-columns" warning
Fix Undefined array key "testdox-columns" warning
2025-10-21 11:44:55 +03:00
Nuno Maduro 08b09f2e98 release: 4.1.2 2025-10-05 20:09:49 +01:00
Nuno Maduro b0fab7e437 chore: uses phpunit@12.4 2025-10-05 20:09:42 +01:00
Nuno Maduro 8e3444e1db chore: bumps requirements 2025-10-01 14:30:25 +01:00
Malico dc9a1e8ace BugFix: Fix toUseTrait to detect inherited and nested traits 2025-09-20 19:06:23 +01:00
nuno maduro fc7a4182b5 adjusts sponsors 2025-09-18 20:13:01 +01:00
Nuno Maduro b7406938ac release: v4.1.0 2025-09-10 14:41:09 +01:00
Nuno Maduro 314caabd1d chore: improves types 2025-09-10 14:41:02 +01:00
Nuno Maduro 65cabf91b1 chore: bumps dependencies 2025-09-10 14:40:52 +01:00
nuno maduro f91c6c1e1e Update social media links in Thanks.php 2025-09-01 00:09:31 +01:00
nuno maduro 843dbbf18a Update README.md 2025-08-31 23:50:16 +01:00
Nuno Maduro 47fb1d7763 release: v4.0.4 2025-08-28 19:19:42 +01:00
Nuno Maduro 639df4cb43 chore: uses phpunit 12.3.7 2025-08-28 19:19:32 +01:00
Nuno Maduro e54e4a0178 release: 4.0.3 2025-08-24 15:17:23 +01:00
Nuno Maduro 7749775f50 chore: uses phpunit 12.3.6 2025-08-24 15:17:19 +01:00
nuno maduro f11f3aa0a4 Merge pull request #1464 from gehrisandro/sandro/prevent-duplicate-attributes
Prevent duplicate attributes
2025-08-23 22:43:15 +01:00
Sandro Gehri 33817013fe Prevent duplicate attributes 2025-08-23 12:35:32 +02:00
Nuno Maduro 7f11ace329 release: 4.0.2 2025-08-22 11:34:24 +01:00
Nuno Maduro 3d776f1f20 fix: revert reading coverage by chunks 2025-08-22 11:12:55 +01:00
Nuno Maduro d5ced0a5ca release: 4.0.1 2025-08-22 09:24:07 +01:00
Nuno Maduro af1e214be4 chore: bumps dependencies 2025-08-22 09:22:16 +01:00
nuno maduro 7f9b50974a Merge pull request #1460 from Admiral-Enigma/4.x
Cast "testdox-columns" to an int
2025-08-22 09:20:44 +01:00
Admiral-Enigma cd5272d8cc Cast "testdox-columns" to an int 2025-08-22 10:00:49 +02:00
Nuno Maduro a7b2039175 Merge branch '3.x' into 4.x 2025-08-20 20:14:15 +01:00
Nuno Maduro 72cf695554 release: 3.8.4 2025-08-20 20:12:42 +01:00
Nuno Maduro 50960a96e9 docs: adjusts release script 2025-08-20 15:30:59 +01:00
Nuno Maduro 507df757a1 release: 4.0.0 2025-08-20 15:29:23 +01:00
Nuno Maduro 8722b3fc3c docs: adjusts readme for 4.x 2025-08-20 15:03:36 +01:00
Nuno Maduro 19eca6e338 fix: skip windows for now 2025-08-20 14:50:15 +01:00
Nuno Maduro 6b523d6963 fix: puts back windows 2025-08-20 14:27:53 +01:00
Nuno Maduro a350545803 fix: windows check 2025-08-20 14:25:49 +01:00
Nuno Maduro 71c2e97c9f chore: bumps dependencies 2025-08-20 14:23:16 +01:00
Nuno Maduro 98a12012bf Merge branch '3.x' into 4.x 2025-08-20 14:22:03 +01:00
Nuno Maduro 027f4e4832 chore: bumps dependencies 2025-08-20 14:21:14 +01:00
Nuno Maduro 92523a6f39 chore: uses phpunit v12.3.5 2025-08-20 08:01:45 +01:00
Nuno Maduro ed38fb644f release: 4.0.0 rc 1 2025-08-20 07:58:51 +01:00
Nuno Maduro 39b66bf01d Merge branch '3.x' into 4.x 2025-08-20 07:04:50 +01:00
Nuno Maduro 165c879fe6 release: 3.8.3 2025-08-19 11:11:21 +01:00
Nuno Maduro 4c8bf4b2fd chore: uses phpunit v11.5.33 2025-08-19 11:11:10 +01:00
Nuno Maduro 1ee36f584d Merge branch '3.x' into 4.x 2025-08-15 17:12:37 +01:00
nuno maduro 1b0a846a81 Update README.md 2025-08-15 17:11:53 +01:00
Nuno Maduro e3e518747f release: beta 2 2025-08-11 15:45:37 +01:00
Nuno Maduro 0b96b8f630 Allows old version of phpunit for now 2025-08-06 13:17:17 +01:00
Nuno Maduro 711a60c2db release: beta 1 2025-08-05 17:44:12 +01:00
Nuno Maduro e7132fa012 fix: removes parse_str from security preset 2025-08-03 16:09:59 -06:00
Nuno Maduro 3b72bbd7fe fix: toMatchObject accept objects 2025-08-03 11:08:11 -06:00
Nuno Maduro 273edb864c chore: adjusts tests 2025-08-03 10:32:14 -06:00
Nuno Maduro fcb60f3c4a chore: improves wording 2025-08-03 09:57:52 -06:00
Nuno Maduro 91bb7589e2 fix: wording on exception 2025-08-03 09:31:31 -06:00
Nuno Maduro e524bf5f73 fix: filter by dataset name 2025-07-30 15:52:40 -06:00
nuno maduro 27414ce19f Merge pull request #1372 from soyuka/patch-1
throw exception instead of using Panic in the TestSuite.php
2025-07-30 18:27:13 +01:00
nuno maduro fbc9e704e2 Merge pull request #1424 from joelbutcher/fix/throw-if-tests-directory-does-not-exist
[3.x] Throw a fatal exception if the tests directory does not exist
2025-07-30 18:26:31 +01:00
Joel Butcher ee6b3ed062 fix: throw a fatal exception if the tests directory does not exist 2025-07-29 19:13:33 +01:00
Nuno Maduro 4c88590b89 feat: not.toHaveSuspiciousCharacters 2025-07-26 07:47:00 -06:00
Nuno Maduro 66e59efec6 Merge branch '3.x' into 4.x 2025-07-26 07:35:23 -06:00
Nuno Maduro f692be3637 chore: bumps dependencies 2025-07-26 07:34:25 -06:00
Nuno Maduro 127ad618d3 chore: style 2025-07-26 07:34:19 -06:00
nuno maduro da04ba62a8 Merge pull request #1287 from mortenscheel/patch-1
Add toNotIncludeSuspiciousCharacters() expectation
2025-07-26 04:30:22 +01:00
Nuno Maduro d187566e63 chore: updates snapshots 2025-07-25 21:29:26 -06:00
nuno maduro 3e86e158b2 Merge pull request #1304 from jshayes/fix/sibling-describe-blocks
Fix an issue with describe blocks with matching names
2025-07-26 04:26:19 +01:00
nuno maduro d6c6489e93 Merge branch '4.x' into fix/sibling-describe-blocks 2025-07-26 04:26:10 +01:00
nuno maduro ee70a3cfea Merge pull request #1319 from pestphp/allow-custom-arch-expectations
Allow custom arch expectations
2025-07-26 04:23:49 +01:00
Nuno Maduro 7a6f33f139 Merge branch '3.x' into 4.x 2025-07-25 21:20:15 -06:00
nuno maduro 55218bcf78 Merge pull request #1324 from bibrokhim/add-attributes-to-laravel-preset
Add Attributes to Laravel preset
2025-07-26 04:19:54 +01:00
Nuno Maduro e29302300f Merge branch '3.x' into 4.x 2025-07-25 21:17:48 -06:00
nuno maduro 2a47b514ec Merge pull request #1351 from cndrsdrmn/patch-1
fix: add ignoring clause for `App\Features\Concerns` on Laravel Preset
2025-07-26 04:17:21 +01:00
Nuno Maduro 222ed174bc style 2025-07-25 21:10:05 -06:00
Nuno Maduro 2aa32569f0 fix: adjust snapshots 2025-07-25 21:06:49 -06:00
nuno maduro 1d8d1a046f Merge pull request #1357 from drsdre/patch-1
toMatchArray/Object wrong field fix
2025-07-26 04:06:12 +01:00
nuno maduro 3d9ceb1cf2 Merge pull request #1373 from clementbirkle/3.x
fix: normalize snapshot paths for tests outside the main tests directory
2025-07-26 03:58:40 +01:00
nuno maduro 520a5fe29d Merge branch '4.x' into 3.x 2025-07-26 03:56:09 +01:00
Nuno Maduro de4409e368 fix: before all 2025-07-25 20:54:37 -06:00
Nuno Maduro 6d6e4e040f fix: wrong status code being used 2025-07-25 18:03:58 -06:00
Nuno Maduro aac08629f7 ci: removes testing against lowest 2025-07-23 08:26:12 +01:00
Nuno Maduro fe27012bbc style 2025-07-22 23:58:36 +01:00
Nuno Maduro f9901245f1 updates dependencies and snapshots 2025-07-22 23:51:32 +01:00
nuno maduro 21e22decf3 Merge pull request #1299 from FaSe22/handle-c-flag
fix: Pest ignores '-c' option
2025-07-22 23:12:07 +01:00
nuno maduro e513f76ea9 Merge pull request #1256 from mazesec/add-slugify-method
Add slugify Method to Str Class and toBeSlug Assertion to Expectation Class
2025-07-22 23:08:15 +01:00
Nuno Maduro be9c95e3bc feat: adds see 2025-07-22 23:06:43 +01:00
nuno maduro 9172721ce8 Merge pull request #1241 from mertasan/ide-reference
feat: Add `references` method for two-way test-source linking
2025-07-22 22:48:26 +01:00
Nuno Maduro 924dc016cc feat: skipLocally 2025-07-22 22:40:38 +01:00
Nuno Maduro f49b91ec0d fixes missing condition 2025-07-22 22:31:08 +01:00
Nuno Maduro 516ace85b4 fix: skipOnCI 2025-07-22 22:08:03 +01:00
Nuno Maduro f9814793dd feat: skipOnCI 2025-07-22 21:52:06 +01:00
Nuno Maduro 00572f5f8e feat: improves playwright 2025-07-22 11:39:34 +01:00
Nuno Maduro fb282b184e fix: return type 2025-07-21 13:32:03 +01:00
Nuno Maduro e0695a13cb feat: adds shell 2025-07-21 13:25:50 +01:00
nuno maduro 8f810bf2a2 Merge pull request #1408 from JonPurvis/remove-language-option
[4.x] Remove Language Option from Profanity Composer Script
2025-07-14 10:37:14 +01:00
nuno maduro 84636cee96 Merge branch '4.x' into remove-language-option 2025-07-14 10:37:08 +01:00
Nuno Maduro 0355119afc fix: snapshots feedback 2025-07-06 14:29:18 +01:00
Nuno Maduro 9d0410ee0b feat: adjusts only for browser debug 2025-07-06 13:45:33 +01:00
Nuno Maduro 0d148c2a67 chroe: bumps phpunit 2025-07-05 15:46:03 +01:00
Nuno Maduro 0f1e87c726 Adds output about sharding 2025-07-05 15:43:43 +01:00
Nuno Maduro 73bf579da3 chore: code refactor 2025-07-02 00:26:15 +01:00
Nuno Maduro 5def62018b fix: shard regex 2025-07-01 11:00:05 +01:00
Nuno Maduro d8e1b27491 ci: fix included version 2025-06-30 22:50:57 +01:00
Nuno Maduro 2ff4713968 ci: fix missing dep 2025-06-30 22:26:09 +01:00
Nuno Maduro 3f27352560 feat: adds --shard 2025-06-30 22:15:07 +01:00
Nuno Maduro af3fdceddb feat: adds phpunit 12.2.5 support 2025-06-28 18:31:45 +01:00
Nuno Maduro 3faeede1ef chore: fixes snapshots 2025-06-28 18:24:19 +01:00
Nuno Maduro 0bc3219a2b feat: moves visit to the core 2025-06-28 18:18:26 +01:00
Nuno Maduro a22013a7d3 fix: with types 2025-06-28 12:14:45 +01:00
Nuno Maduro 7fc69033f8 chore: adjusts style 2025-06-27 02:15:36 +01:00
Nuno Maduro ef76c04dbe feat: adds fixture 2025-06-27 02:15:28 +01:00
Nuno Maduro 7d77bbf1bb Merge pull request #1410 from JonPurvis/remove-period
Remove Period from `ShouldNotHappen` message
2025-06-23 19:29:01 +01:00
Nuno Maduro 163479ae60 chore: style 2025-06-16 10:14:16 +01:00
Nuno Maduro c3bfdf130e chore: type checking 2025-06-16 10:14:04 +01:00
Nuno Maduro 8c403a57c2 fix: upper case fix 2025-06-16 10:04:00 +01:00
JonPurvis 97c136cd94 link to issues page 2025-06-16 02:55:11 +01:00
JonPurvis d6cbd12d8b remove period from message 2025-06-16 02:51:48 +01:00
Nuno Maduro 49bf00024f fix: coverage when coverage file is over 2.4gb on mac os 2025-06-15 22:43:59 +01:00
JonPurvis dd44ac4195 remove language option from profanity composer script 2025-06-14 21:36:39 +01:00
Nuno Maduro 5d2aafd2a3 feat: --profanity 2025-06-12 00:38:05 +01:00
Nuno Maduro 0fc9d4dfe0 feat: adds phpunit 12.2.1 support 2025-06-08 15:29:23 +01:00
Nuno Maduro 02b1ffb334 chore: bump dependencies 2025-05-27 11:37:29 +01:00
Nuno Maduro c62cc3fef0 chore: adds pokio 2025-05-23 05:19:56 +01:00
Nuno Maduro 909d778da3 fix: undefined property 2025-05-21 02:00:15 +01:00
Nuno Maduro 7711a52fe9 Bumps dependencies 2025-05-09 13:10:29 +01:00
Nuno Maduro 99c9f4e5d8 Bumps dependencies 2025-05-03 11:58:03 +01:00
Nuno Maduro a310796165 Fixes filtering tests 2025-04-29 11:38:33 +01:00
Nuno Maduro db9243ca2e bump dependencies 2025-04-29 09:57:02 +01:00
Nuno Maduro 635e3b4c41 chore: deprecates php 8.2 2025-04-20 23:02:19 +01:00
Nuno Maduro 791734a29c Fixes tests 2025-04-20 22:19:25 +01:00
Nuno Maduro 8cfb0acf46 bump paratest 2025-04-20 21:51:01 +01:00
Nuno Maduro bf67407ba5 Merge pull request #1391 from MrPunyapal/feat/phpunit-12
[WIP] Feat: PHPUnit 12
2025-04-20 21:35:32 +01:00
Nuno Maduro efdc84e115 Merge branch '4.x' into feat/phpunit-12 2025-04-20 21:35:25 +01:00
Nuno Maduro d1608bf33d chore: prepares for 4.x 2025-04-20 21:33:50 +01:00
Punyapal Shah 4f6140fdb1 refactor: move test case initialization to a separate method in Testable trait 2025-04-20 15:37:02 +05:30
Punyapal Shah 442a58d07f refactor: comment arch presets in Arch.php 2025-04-20 15:19:56 +05:30
Punyapal Shah 19e9267021 fix: update PHPUnit version 2025-04-20 15:19:40 +05:30
Nuno Maduro c6244a8712 Release 3.8.2 2025-04-17 11:53:02 +01:00
Nuno Maduro eed68f2840 Adjusts sponsors 2025-04-13 17:15:23 +01:00
Nuno Maduro 6080f51a0b release: v3.8.1 2025-04-03 17:35:58 +01:00
Nuno Maduro e0f07be017 fix: init command detecting laravel 2025-04-03 17:23:39 +01:00
Nuno Maduro 42e1b9f17f release: v3.8.0 2025-03-30 18:49:10 +01:00
Nuno Maduro 0171617c1d chore: adjusts to new types on arch 2025-03-30 18:42:00 +01:00
Nuno Maduro 2e11e9e65d docs: adjusts readme 2025-03-29 18:23:23 +00:00
Nuno Maduro 4969526ef2 chore: bumps paratest 2025-03-29 17:57:53 +00:00
Nuno Maduro d7b1c36fdd Merge pull request #1341 from nuernbergerA/phpunit-overrides
chore: Sync overrides
2025-03-29 17:52:57 +00:00
Nuno Maduro 003fc96e8f release: 3.7.5 2025-03-29 17:48:00 +00:00
Nuno Maduro f68d11ccae chore: bumps dependencies 2025-03-29 17:44:06 +00:00
Clément Birklé e46d499384 add tests for snapshots external to the tests directory 2025-03-19 16:33:44 +01:00
Clément Birklé 490f321a0d fix: normalize snapshot paths for files outside tests directory 2025-03-19 15:59:45 +01:00
Antoine Bluchet 174645caa2 throw exception instead of using Panic 2025-03-18 14:49:32 +01:00
Nuno Maduro ed70c9dc2b refactor: type adjustments 2025-03-14 22:40:39 +00:00
Nuno Maduro 157a753d87 Update Pest.php.stub 2025-03-12 18:35:57 +00:00
AFS 2c3a53f6cd Remove reset of message
Reset approach was not the right one.
2025-02-25 15:44:32 +01:00
AFS 0bdaef29e9 Fix lingering }
Remove remaining } in toMatchObject.
2025-02-25 15:41:17 +01:00
AFS 1ad30a97b3 toMatchArray/Object wrong field fix
The functions toMatchArray and toMatchObject indicate that the wrong field is mismatching from the second loop on because the 'message' is overwritten and taken into the following loop. This patch creates a $second_message for the second test (value test) to keep the error message correct.
2025-02-25 15:34:37 +01:00
Candra Sudirman a5317c5640 fix: add ignoring clause for App\Features\Concerns on Laravel Preset 2025-02-08 00:07:21 +07:00
Nuno Maduro 66ceb64faa Updates tests 2025-02-03 13:36:47 +00:00
Nuno Maduro fa4098db8d Bumps dependencies 2025-02-03 13:30:45 +00:00
Nuno Maduro 4a987d3d5c release: 3.7.4 2025-01-23 14:03:29 +00:00
Nuno Maduro 4079a08f5f feat: adds --compact to coverage 2025-01-23 13:59:51 +00:00
Nuno Maduro e4aab77a34 release: 3.7.3 2025-01-23 12:51:02 +00:00
jshayes 50ff347b59 Pass description into describe call 2025-01-20 17:40:18 -05:00
jshayes b5b8fab09b Fix an issue where beforeEach/afterEach functions were called on other describe blocks with the same name 2025-01-20 17:36:22 -05:00
Adrian Nürnberger c4c9e915f4 cs 2025-01-20 09:50:36 +01:00
Adrian Nürnberger e834527db2 Update JunitXmlLogger.php
https://github.com/sebastianbergmann/phpunit/issues/6098
2025-01-20 09:39:10 +01:00
Adrian Nürnberger 23f130b0f9 Update JunitXmlLogger.php
from https://github.com/sebastianbergmann/phpunit/issues/5771
https://github.com/sebastianbergmann/phpunit/commit/c722fb259972a6c55fbb295169fd1a000b3d4c88
2025-01-20 09:38:24 +01:00
Adrian Nürnberger 0cb8c42497 sync missing listener 2025-01-20 09:36:48 +01:00
Adrian Nürnberger fe4b5e5e1f sync change 2025-01-20 09:35:44 +01:00
Adrian Nürnberger 8ee9d66d80 sync cs 2025-01-20 09:34:55 +01:00
Adrian Nürnberger 7760d945bb sync latest changes 2025-01-20 09:34:23 +01:00
Nuno Maduro 709ecb1ba2 chore: adjusts tests 2025-01-19 17:35:09 +00:00
Nuno Maduro 6afb36519d release: 3.7.2 2025-01-19 17:16:25 +00:00
Nuno Maduro 150bb9478d docs: adjusts sponsors 2025-01-08 01:09:20 +00:00
Nuno Maduro bf3178473d release: 3.7.1 2024-12-12 11:52:01 +00:00
Nuno Maduro d2eb94d723 chore: bumps phpunit and paratest 2024-12-12 11:50:43 +00:00
Nuno Maduro 9688b83a3d release: 3.7.0 2024-12-10 11:54:49 +00:00
Nuno Maduro 675372c794 chore: fixes types 2024-12-10 11:54:42 +00:00
Nuno Maduro c18636b3d5 chore: adds phpunit 11.5.0 support 2024-12-10 11:53:59 +00:00
Bakhromjon 1ac594bdf0 Add Attributes to Laravel preset 2024-12-06 16:07:59 +05:00
Nuno Maduro 145294a4a3 chore: style 2024-12-01 23:55:15 +00:00
Nuno Maduro c2cabaeae6 chore: fixes test suite 2024-12-01 23:16:34 +00:00
Nuno Maduro 918a8fc169 release: 3.6.0 2024-12-01 22:46:00 +00:00
Nuno Maduro 5d32dd0641 feat: option to coverage 2024-12-01 22:45:31 +00:00
Nuno Maduro 982353fb38 release: 3.5.2 2024-12-01 21:28:14 +00:00
Nuno Maduro 2eefa8b88d chore: adds phpunit 11.4.4 support 2024-12-01 21:26:11 +00:00
Nuno Maduro 787d5492ac chore: ignores temporary file 2024-12-01 20:41:34 +00:00
Nuno Maduro 06a0bd9b0b Bumps dependencies 2024-11-21 10:46:27 +00:00
olivernybroe 5331b44a18 Allow custom arch expectations 2024-11-20 11:54:36 +01:00
Nuno Maduro 91afc81222 Updates sponsors 2024-11-09 14:20:12 +00:00
Nuno Maduro 179d46ce97 release: v3.5.1 2024-10-31 12:12:45 -04:00
Nuno Maduro fa2bc1e536 chore: bumps dependencies 2024-10-31 12:12:37 -04:00
Nuno Maduro eaeb133c77 release: v3.5.0 2024-10-22 15:33:27 +01:00
Nuno Maduro cf57ea1f94 Merge pull request #1295 from jshayes/nested-describe
Support for nested describe blocks
2024-10-22 13:41:38 +01:00
Nuno Maduro 0b7f4f2384 Merge pull request #1301 from faissaloux/update-tests-badge
Update tests badge to v3
2024-10-20 12:48:07 +01:00
Nuno Maduro 2903a7e621 release: v3.4.2 2024-10-20 12:47:25 +01:00
Nuno Maduro b8964375c7 release: v3.4.1 2024-10-20 12:43:35 +01:00
Nuno Maduro bdcb883829 chore: bumps phpunit version 2024-10-20 12:43:28 +01:00
faissaloux 8a7e7f39ef update tests badge to v3 2024-10-17 02:33:25 +01:00
sebastianfaber 53c94600cb fix: handle -c flag same as --configuration 2024-10-16 22:19:08 +02:00
Nuno Maduro 67f217852c chore: uses stable version of collision and termwind 2024-10-15 17:17:09 +01:00
Nuno Maduro 1bad148487 release: v3.4.0 2024-10-15 15:13:19 +01:00
Nuno Maduro e24f137b8e fix: deprecation 2024-10-15 15:09:26 +01:00
Nuno Maduro 6d9189f3f5 feat: php 8.4 support 2024-10-15 15:04:30 +01:00
jshayes 6968094e2b Add tests 2024-10-13 10:39:17 -04:00
jshayes 9510d4a2f9 Change array type hint 2024-10-13 09:54:19 -04:00
jshayes cd2eb3504b Add helper to get last element of array 2024-10-13 09:47:54 -04:00
jshayes 7c639cdbbd Add more tests 2024-10-13 00:41:04 -04:00
Nuno Maduro 1513ede73b release: v3.3.2 2024-10-12 12:36:44 +01:00
Nuno Maduro 8c65197881 chore: bumps depndencies 2024-10-12 12:33:57 +01:00
jshayes a6cd83665c Execute all parent beforeEach and afterEach functions for each test 2024-10-11 23:51:18 -04:00
jshayes 0c57142c03 Fix an issue where a describe block will prevent a beforeEach call from executing 2024-10-11 21:24:08 -04:00
Nuno Maduro 3f65af9fdf Merge pull request #1292 from olivernybroe/policy-suffix
Add Policy suffix to policies
2024-10-11 09:34:05 +01:00
Nuno Maduro 42d89814e3 Merge pull request #1293 from AbdellahBoutmad/dir
modify test command in contrbuting.md
2024-10-11 09:33:41 +01:00
Nuno Maduro 1e3156a5b6 release: v3.3.1 2024-10-11 09:31:24 +01:00
Nuno Maduro 97713c0832 chore: bumps dependencies 2024-10-11 09:31:16 +01:00
abdellah boutmad 62b0e3c9df modify test command in contrbuting.md 2024-10-10 12:34:04 +01:00
olivernybroe 647de2f1cf Add Policy suffix to policies 2024-10-10 08:08:10 +02:00
Nuno Maduro 0a7bff0d24 release: v3.3.0 2024-10-06 19:25:27 +01:00
Nuno Maduro 7618434580 chore: uses phpunit v11.4.0 2024-10-06 19:25:20 +01:00
Morten Scheel dd7d150caa Add toNotIncludeSuspiciousCharacters() expectation 2024-10-04 13:44:10 +02:00
Nuno Maduro 1e0bb88b73 release: v3.2.5 2024-10-01 11:55:18 +01:00
Nuno Maduro 83b76d7c2e chore: bumps dependencies 2024-10-01 11:51:26 +01:00
Nuno Maduro 5a870b3940 chore: style changes 2024-10-01 11:51:16 +01:00
Nuno Maduro 1115c64186 chore: style changes 2024-10-01 11:48:14 +01:00
Nuno Maduro e38a271ca2 Merge pull request #1279 from midnite81/bug/declare-strict-types-with-comments-above
Strict types expectation allows for comments above declaration
2024-09-29 19:12:06 +01:00
Nuno Maduro 43703ab40a Merge pull request #1280 from CamKem/fix/middleware-method-preset
Fix: Add middleware to the allowable public methods for Laravel Preset
2024-09-29 19:11:40 +01:00
Cam Kemshal-Bell 86452765a4 fix: add middleware to the allowable public methods on the laravel preset 2024-09-29 16:07:39 +10:00
Simon Rogers b8a1b7e5cc Add tests for strict types expectation
Introduced new test cases to ensure strict type declaration handling. Files with and without strict types are tested, including scenarios with comments preceding the declaration. Updated the regex in `Expectation.php` to accommodate comments and whitespaces before the `declare(strict_types=1)` statement.
2024-09-28 17:31:33 +01:00
Nuno Maduro 5fe79d9c18 release: v3.2.4 2024-09-26 23:53:39 +01:00
Nuno Maduro 2744da4292 Merge pull request #1277 from MuhammedAlkhudiry/ignore-handler-in-laravel-preset
ignore App\Exceptions\Handler.php in arch laravel preset
2024-09-26 23:47:26 +01:00
Muhammed Alkhudiry 87f4e5e7b3 fix 2024-09-26 16:44:30 +03:00
Muhammed Alkhudiry bb3decf3cc ignore App\Exceptions\Handler.php in arch laravel preset 2024-09-26 16:41:43 +03:00
Nuno Maduro 4e2987d438 release: v3.2.3 2024-09-25 16:19:39 +01:00
Nuno Maduro a25158bce8 Merge pull request #1275 from jeremynikolic/laravel-presets-ignore-concerns
feat: add ignoring of Concerns folder inside App\Enums and App\Features
2024-09-25 16:16:26 +01:00
Jeremy Nikolic 49e77b1d4c feat: add ignoring of Concerns folder inside App\Enums and App\Features 2024-09-25 17:12:42 +02:00
Nuno Maduro 989e43d1a0 release: v3.2.2 2024-09-24 10:23:43 +01:00
Nuno Maduro 7cd42aafd8 fix: auto-complete on presets 2024-09-24 10:23:32 +01:00
Nuno Maduro 48a1de273f release: v3.2.1 2024-09-23 14:09:55 +01:00
Nuno Maduro 970e16e949 Ignores 2024-09-23 14:08:30 +01:00
Nuno Maduro 432ff221c6 fix: missing != and !== on new toUseStrictEquality arch expectation 2024-09-23 14:08:21 +01:00
tal7aouy 92bc1decd9 Add tests for toBeSlug method 2024-09-16 13:41:13 +01:00
tal7aouy e3bfcbe5f1 Add slugify method 2024-09-16 13:36:34 +01:00
Mert Aşan ba7eb70a5d Remove unnecessary property 2024-09-12 13:11:12 +03:00
Mert Aşan 74ff3b8cd9 Update src/PendingCalls/TestCall.php
Co-authored-by: Owen Voke <development@voke.dev>
2024-09-12 13:09:57 +03:00
Mert Aşan ab0b4a1b4e Update src/PendingCalls/TestCall.php
Co-authored-by: Owen Voke <development@voke.dev>
2024-09-12 13:09:45 +03:00
Mert Aşan 169b76458e make the name of the method plural 2024-09-12 11:15:30 +03:00
Mert Aşan 668685498f Fix phpdoc type-hints 2024-09-12 03:51:20 +03:00
Mert Aşan bab193e7e1 Fix property type 2024-09-12 03:24:36 +03:00
Mert Aşan f720be862e Add reference method 2024-09-12 02:45:37 +03:00
634 changed files with 26120 additions and 5257 deletions
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
## Reporting a Vulnerability
If you discover a security vulnerability in Pest, please report it privately using one of the following channels:
1. **GitHub Private Vulnerability Reporting** (preferred) — go to the repository's **Security** tab and click **"Report a vulnerability"**. This creates a private advisory visible only to maintainers and provides a structured workflow for triage, fix coordination, and CVE assignment.
2. **Email** — send the details to Nuno Maduro at **enunomaduro@gmail.com**.
All security vulnerabilities will be promptly addressed.
+12
View File
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 5
groups:
github-actions:
patterns:
- "*"
+37 -11
View File
@@ -2,9 +2,17 @@ name: Static Analysis
on: on:
push: push:
branches: [5.x]
pull_request: pull_request:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 9 * * *'
concurrency:
group: static-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs: jobs:
static: static:
@@ -12,33 +20,51 @@ jobs:
name: Static Tests name: Static Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
dependency-version: [prefer-lowest, prefer-stable] dependency-version: [prefer-stable]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with: with:
php-version: 8.2 php-version: 8.4
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
extensions: sockets
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: static-php-8.4-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json', '**/composer.lock') }}
restore-keys: |
static-php-8.4-${{ matrix.dependency-version }}-composer-
static-php-8.4-composer-
- name: Install Dependencies - name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress --ansi env:
COMPOSER_ROOT_VERSION: 5.x-dev
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
# - name: Type Check - name: Profanity Check
# run: composer test:type:check run: composer test:profanity
- name: Type Check
run: composer test:type:check
- name: Type Coverage - name: Type Coverage
run: composer test:type:coverage run: composer test:type:coverage
- name: Refacto
run: composer test:refacto
- name: Style - name: Style
run: composer test:lint run: composer test:lint
+35 -13
View File
@@ -2,33 +2,59 @@ name: Tests
on: on:
push: push:
branches: [5.x]
pull_request: pull_request:
schedule:
- cron: '0 9 * * *'
concurrency:
group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs: jobs:
tests: tests:
if: github.event_name != 'schedule' || github.repository == 'pestphp/pest' if: github.event_name != 'schedule' || github.repository == 'pestphp/pest'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest] # windows-latest
symfony: ['7.1'] symfony: ['8.0']
php: ['8.2', '8.3', '8.4'] php: ['8.4', '8.5']
dependency_version: [prefer-lowest, prefer-stable] dependency_version: [prefer-stable]
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
extensions: sockets
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
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') }}
restore-keys: |
${{ matrix.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-
${{ matrix.os }}-php-${{ matrix.php }}-composer-
- name: Setup Problem Matches - name: Setup Problem Matches
run: | run: |
@@ -37,12 +63,9 @@ jobs:
- name: Install PHP dependencies - name: Install PHP dependencies
shell: bash shell: bash
run: | env:
if [[ "${{ matrix.php }}" == "8.4" ]]; then COMPOSER_ROOT_VERSION: 5.x-dev
composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}" --ignore-platform-req=php run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
else
composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
fi
- name: Unit Tests - name: Unit Tests
run: composer test:unit run: composer test:unit
@@ -51,5 +74,4 @@ jobs:
run: composer test:parallel run: composer test:parallel
- name: Integration Tests - name: Integration Tests
if: ${{ matrix.php != '8.4' }}
run: composer test:integration run: composer test:integration
+3 -1
View File
@@ -1,7 +1,7 @@
.idea/* .idea/*
.idea/codeStyleSettings.xml .idea/codeStyleSettings.xml
.temp/* .temp/*
composer.lock /composer.lock
/vendor/ /vendor/
coverage.xml coverage.xml
.phpunit.result.cache .phpunit.result.cache
@@ -12,3 +12,5 @@ coverage.xml
*.swp *.swp
*.swo *.swo
.vscode/ .vscode/
.STREAM.md
+34
View File
@@ -0,0 +1,34 @@
# CLAUDE.md
**Do not edit this file.** Agents must never add, remove, or reword anything here. If a change seems needed, say so and let a human do it.
## Ask before testing
When asked to review code or build a feature, do not run the test suite and do not write new tests. Make the change, report it, then ask the user whether tests should be added — describing the tests you have in mind — and wait for the user to confirm.
Two reasons this matters here: the suite takes minutes, and `tests/.snapshots/success.txt` plus the tally in `tests/Visual/Parallel.php` encode the whole suite's result, so a single added test breaks both.
Once the user confirms:
```bash
composer test:unit # fast, excludes the visual group
composer test:integration # visual and snapshot tests
composer test # everything CI runs, in CI's order
composer update:snapshots # only when a test was added or removed
```
## TIA scenario tests
`tests/Features/Tia/*` scaffold a throwaway git project, run a real `pest` subprocess against it, and diff the TIA graph it wrote. They exist because TIA's contract is about what a run *writes* — replay, branch keys, and the COMPLETE / RESULTS-ONLY / HARD-SUPPRESSED tiers are invisible to ordinary assertions, and every case used to be measured by hand against a playground app.
Add one whenever a change touches branch resolution, replay, filtered mode, or the write tiers. How:
- `Project::make('master')` scaffolds; `seed('master')` writes a graph and sentinels every cached result (`time=9.999`, `assertions=42`) so any rewrite shows up.
- `$project->pest('--tia', …)` runs it; `$project->delta()` compares against that snapshot. `writtenCount()` is the discriminator — `0` means "replayed", not "wrote the same values". `mutateGraph()` bends one entry; overlays in `tests/Fixtures/Tia/overlays/<name>/` supply a different `tests/Pest.php`.
- Keep expectations driver-independent: a cold recording run needs pcov/xdebug and behaves differently without one. Seed a graph instead of recording one.
Run them by file (a directory argument finds nothing) or by `--filter`:
```bash
php bin/pest tests/Features/Tia/PartialRunWriteTier.php
```
+1 -1
View File
@@ -42,7 +42,7 @@ composer test
Check types: Check types:
```bash ```bash
composer test:types composer test:type:check
``` ```
Unit tests: Unit tests:
-14
View File
@@ -1,14 +0,0 @@
# Well documented Makefiles
DEFAULT_GOAL := help
help:
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
build: ## Build all docker images. Specify the command e.g. via make build ARGS="--build-arg PHP=8.2"
docker compose build $(ARGS)
##@ [Application]
install: ## Install the composer dependencies
docker compose run --rm composer install
test: ## Run the tests
docker compose run --rm composer test
+26 -23
View File
@@ -1,43 +1,46 @@
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/v3/banner.png" width="600" alt="PEST"> <img src="https://raw.githubusercontent.com/pestphp/art/master/v5/social.png" width="600" alt="PEST">
<p align="center"> <p align="center">
<a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (master)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=2.x&label=Tests%202.x"></a> <a href="https://github.com/pestphp/pest/actions"><img alt="GitHub Workflow Status (5.x)" src="https://img.shields.io/github/actions/workflow/status/pestphp/pest/tests.yml?branch=5.x&label=Tests%205.x"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="Total Downloads" src="https://img.shields.io/packagist/dt/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="Latest Version" src="https://img.shields.io/packagist/v/pestphp/pest"></a>
<a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a> <a href="https://packagist.org/packages/pestphp/pest"><img alt="License" src="https://img.shields.io/packagist/l/pestphp/pest"></a>
<a href="https://whyphp.dev"><img src="https://img.shields.io/badge/Why_PHP-in_2026-7A86E8?style=flat-square&labelColor=18181b" alt="Why PHP in 2026"></a>
<a href="https://youtube.com/@nunomaduro?sub_confirmation=1"><img alt="YouTube Channel Subscribers" src="https://img.shields.io/youtube/channel/subscribers/UCO_hYZF2gb_CyG5sA7ArlGg?style=flat&label=youtube&color=brightgreen"></a>
</p> </p>
</p> </p>
------ ------
> Pest v3 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest3-now-available)**. > Pest v5 Now Available: **[Read the announcement »](https://pestphp.com/docs/pest5-now-available)**.
**Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. **Pest** is an elegant testing framework for PHP developers and AI agents.
- Explore our docs at **[pestphp.com »](https://pestphp.com)** - Explore our docs at **[pestphp.com »](https://pestphp.com)**
- Follow us on Twitter at **[@pestphp »](https://twitter.com/pestphp)** - Follow the creator Nuno Maduro:
- Join us at **[discord.gg/kaHY6p54JH »](https://discord.gg/kaHY6p54JH)** or **[t.me/+kYH5G4d5MV83ODk0 »](https://t.me/+kYH5G4d5MV83ODk0)** - YouTube: **[youtube.com/@nunomaduro](https://youtube.com/@nunomaduro)** — Videos every week
- Twitch: **[twitch.tv/nunomaduro](https://twitch.tv/nunomaduro)** — Live coding on Mondays, Wednesdays, and Fridays at 9PM UTC
- Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)**
- LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)**
- Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)**
- Tiktok: **[tiktok.com/@enunomaduro](https://www.tiktok.com/@enunomaduro)**
## Sponsors ## Sponsors
We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**. We cannot thank our sponsors enough for their incredible support in funding Pest's development. Their contributions have been instrumental in making Pest the best it can be. For those who are interested in becoming a sponsor, please visit Nuno Maduro's Sponsor page at **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)**.
### Platinum Sponsors - **[PhpStorm](https://jb.gg/nuno)**
- **[CodeRabbit](https://coderabbit.link/nunomaduro)**
- **[LaraJobs](https://larajobs.com)** - **[SerpApi](https://serpapi.com/?ref=nunomaduro)**
- **[Brokerchooser](https://brokerchooser.com)** - **[Bento](https://bentonow.com/?ref=nunomaduro)**
- **[Forge](https://forge.laravel.com)** - **[CMS Max](https://cmsmax.com/?ref=nunomaduro)**
- **[Spatie](https://spatie.be)** - **[Redberry](https://redberry.international/laravel-development/?utm_source=pest&utm_medium=banner&utm_campaign=pest_sponsorship)**
- **[Worksome](https://www.worksome.com/)** - **[Typesense](https://typesense.org/?ref=nunomaduro)**
- **[Pixel](https://wearepixel.com.au/?ref=nunomaduro)**
### Premium Sponsors - [TestMu AI](https://www.testmuai.com/?utm_medium=sponsor&utm_source=pest)
- [Zapiet](https://zapiet.com/?ref=nunomaduro)
- [Akaunting](https://akaunting.com/?ref=pestphp) - [LoadForge](https://loadforge.com/?ref=nunomaduro)
- [Codecourse](https://codecourse.com/?ref=pestphp) - [Route4Me](https://route4me.com/?ref=nunomaduro)
- [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp) - [Nerdify](https://getnerdify.com/?ref=nunomaduro)
- [Laracasts](https://laracasts.com/?ref=pestphp)
- [Localazy](https://localazy.com/?ref=pestphp)
- [Route4Me](https://www.route4me.com/?ref=pestphp)
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
+3 -3
View File
@@ -2,10 +2,10 @@
When releasing a new version of Pest there are some checks and updates that need to be done: When releasing a new version of Pest there are some checks and updates that need to be done:
> **For Pest v2 you should use the `2.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 3.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}...3.x](https://github.com/pestphp/pest/compare/{latest_version}...3.x) - On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...5.x](https://github.com/pestphp/pest/compare/{latest_version}...5.x)
- Update the version number in [src/Pest.php](src/Pest.php) - Update the version number in [src/Pest.php](src/Pest.php)
- Run the tests locally using: `composer test` - Run the tests locally using: `composer test`
- Commit the Pest file with the message: `git commit -m "release: vX.X.X"` - Commit the Pest file with the message: `git commit -m "release: vX.X.X"`
+35 -11
View File
@@ -1,13 +1,18 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php declare(strict_types=1); <?php
declare(strict_types=1);
use Pest\Contracts\Restarter;
use Pest\Kernel; use Pest\Kernel;
use Pest\Panic; use Pest\Panic;
use Pest\Support\Container;
use Pest\TestCaseFilters\GitDirtyTestCaseFilter; use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
use Pest\TestCaseMethodFilters\AssigneeTestCaseFilter; use Pest\TestCaseMethodFilters\AssigneeTestCaseFilter;
use Pest\TestCaseMethodFilters\IssueTestCaseFilter; use Pest\TestCaseMethodFilters\IssueTestCaseFilter;
use Pest\TestCaseMethodFilters\NotesTestCaseFilter; use Pest\TestCaseMethodFilters\NotesTestCaseFilter;
use Pest\TestCaseMethodFilters\PrTestCaseFilter; use Pest\TestCaseMethodFilters\PrTestCaseFilter;
use Pest\TestCaseMethodFilters\FlakyTestCaseFilter;
use Pest\TestCaseMethodFilters\TodoTestCaseFilter; use Pest\TestCaseMethodFilters\TodoTestCaseFilter;
use Pest\TestSuite; use Pest\TestSuite;
use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArgvInput;
@@ -21,6 +26,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
$dirty = false; $dirty = false;
$todo = false; $todo = false;
$flaky = false;
$notes = false; $notes = false;
foreach ($arguments as $key => $value) { foreach ($arguments as $key => $value) {
@@ -37,7 +43,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--test-directory=')) { if (str_contains($value, '--test-directory=')) {
unset($arguments[$key]); unset($arguments[$key]);
} else if ($value === '--test-directory') { } elseif ($value === '--test-directory') {
unset($arguments[$key]); unset($arguments[$key]);
if (isset($arguments[$key + 1])) { if (isset($arguments[$key + 1])) {
@@ -55,6 +61,11 @@ use Symfony\Component\Console\Output\ConsoleOutput;
unset($arguments[$key]); unset($arguments[$key]);
} }
if ($value === '--flaky') {
$flaky = true;
unset($arguments[$key]);
}
if ($value === '--notes') { if ($value === '--notes') {
$notes = true; $notes = true;
unset($arguments[$key]); unset($arguments[$key]);
@@ -62,7 +73,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--assignee=')) { if (str_contains($value, '--assignee=')) {
unset($arguments[$key]); unset($arguments[$key]);
} else if ($value === '--assignee') { } elseif ($value === '--assignee') {
unset($arguments[$key]); unset($arguments[$key]);
if (isset($arguments[$key + 1])) { if (isset($arguments[$key + 1])) {
@@ -72,7 +83,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--issue=')) { if (str_contains($value, '--issue=')) {
unset($arguments[$key]); unset($arguments[$key]);
} else if ($value === '--issue') { } elseif ($value === '--issue') {
unset($arguments[$key]); unset($arguments[$key]);
if (isset($arguments[$key + 1])) { if (isset($arguments[$key + 1])) {
@@ -82,7 +93,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--ticket=')) { if (str_contains($value, '--ticket=')) {
unset($arguments[$key]); unset($arguments[$key]);
} else if ($value === '--ticket') { } elseif ($value === '--ticket') {
unset($arguments[$key]); unset($arguments[$key]);
if (isset($arguments[$key + 1])) { if (isset($arguments[$key + 1])) {
@@ -92,7 +103,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--pr=')) { if (str_contains($value, '--pr=')) {
unset($arguments[$key]); unset($arguments[$key]);
} else if ($value === '--pr') { } elseif ($value === '--pr') {
unset($arguments[$key]); unset($arguments[$key]);
if (isset($arguments[$key + 1])) { if (isset($arguments[$key + 1])) {
@@ -102,7 +113,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--pull-request=')) { if (str_contains($value, '--pull-request=')) {
unset($arguments[$key]); unset($arguments[$key]);
} else if ($value === '--pull-request') { } elseif ($value === '--pull-request') {
unset($arguments[$key]); unset($arguments[$key]);
if (isset($arguments[$key + 1])) { if (isset($arguments[$key + 1])) {
@@ -117,7 +128,6 @@ use Symfony\Component\Console\Output\ConsoleOutput;
} }
} }
// Used when Pest is required using composer. // Used when Pest is required using composer.
$vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php'; $vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php';
@@ -134,7 +144,8 @@ use Symfony\Component\Console\Output\ConsoleOutput;
// Get $rootPath based on $autoloadPath // Get $rootPath based on $autoloadPath
$rootPath = dirname($autoloadPath, 2); $rootPath = dirname($autoloadPath, 2);
$input = new ArgvInput();
$input = new ArgvInput;
$testSuite = TestSuite::getInstance( $testSuite = TestSuite::getInstance(
$rootPath, $rootPath,
@@ -146,11 +157,15 @@ use Symfony\Component\Console\Output\ConsoleOutput;
} }
if ($todo) { if ($todo) {
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter()); $testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter);
}
if ($flaky) {
$testSuite->tests->addTestCaseMethodFilter(new FlakyTestCaseFilter);
} }
if ($notes) { if ($notes) {
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter()); $testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter);
} }
if ($assignee = $input->getParameterOption('--assignee')) { if ($assignee = $input->getParameterOption('--assignee')) {
@@ -180,6 +195,15 @@ use Symfony\Component\Console\Output\ConsoleOutput;
try { try {
$kernel = Kernel::boot($testSuite, $input, $output); $kernel = Kernel::boot($testSuite, $input, $output);
$container = Container::getInstance();
foreach (Kernel::RESTARTERS as $restarterClass) {
$restarter = $container->get($restarterClass);
assert($restarter instanceof Restarter);
$restarter->maybeRestart($rootPath, $originalArguments);
}
$result = $kernel->handle($originalArguments, $arguments); $result = $kernel->handle($originalArguments, $arguments);
$kernel->terminate(); $kernel->terminate();
+374
View File
@@ -0,0 +1,374 @@
#!/usr/bin/env node
import { readdir, readFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { createRequire } from 'node:module'
import { resolve, relative, extname, sep, join } from 'node:path'
import { pathToFileURL } from 'node:url'
const PAGE_EXTENSIONS = new Set([
'.vue', '.svelte',
'.tsx', '.jsx',
'.ts', '.js',
'.mts', '.cts', '.mjs', '.cjs',
])
const ASSET_EXT_RE = /\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|eot|otf|md|mdx|txt|html|mp4|webm|mp3|wav|ogg|m4a|pdf|wasm|glsl|frag|vert)$/i
const PROJECT_ROOT = resolve(process.argv[2] ?? process.cwd())
const PAGE_DIR_CANDIDATES = [
'resources/js/Pages',
'resources/js/pages',
'assets/js/Pages',
'assets/js/pages',
'assets/Pages',
'assets/pages',
]
async function loadRolldown() {
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
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')
return JSON.parse(stripJsonComments(raw))
}
export async function loadAliasFromTsconfig(projectRoot = PROJECT_ROOT) {
const alias = {}
for (const name of ['tsconfig.json', 'jsconfig.json']) {
const p = join(projectRoot, name)
if (!existsSync(p)) continue
let cfg
try { cfg = await readJsonWithComments(p) } catch { continue }
const baseUrl = resolve(projectRoot, cfg?.compilerOptions?.baseUrl ?? '.')
const paths = cfg?.compilerOptions?.paths ?? {}
for (const [key, targets] of Object.entries(paths)) {
if (!key.endsWith('/*')) continue
const t0 = Array.isArray(targets) ? targets[0] : null
if (typeof t0 !== 'string' || !t0.endsWith('/*')) continue
const aliasKey = key.slice(0, -2)
if (alias[aliasKey] !== undefined) continue
alias[aliasKey] = resolve(baseUrl, t0.slice(0, -2))
}
}
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 []
const out = []
const walk = async (dir) => {
let entries
try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }
for (const entry of entries) {
const full = resolve(dir, entry.name)
if (entry.isDirectory()) { await walk(full); continue }
if (PAGE_EXTENSIONS.has(extname(entry.name))) out.push(full)
}
}
await walk(pagesDir)
return out
}
async function discoverPagesDir() {
const override = process.env.TIA_VITE_PAGES_DIR
if (override && override.length > 0) {
return resolve(PROJECT_ROOT, override.replace(/\\/g, '/'))
}
for (const rel of PAGE_DIR_CANDIDATES) {
const abs = resolve(PROJECT_ROOT, rel)
if (!existsSync(abs)) continue
const files = await listPageFiles(abs)
if (files.length > 0) return abs
}
return null
}
function componentNameFor(pageAbs, pagesDir) {
const rel = relative(pagesDir, pageAbs).split(sep).join('/')
const ext = extname(rel)
return rel.slice(0, rel.length - ext.length)
}
function isLocalSpecifier(source, aliasKeys) {
if (source.startsWith('.') || source.startsWith('/')) return true
for (const key of aliasKeys) {
if (source === key || source.startsWith(key + '/')) return true
}
return false
}
async function main() {
const pagesDir = await discoverPagesDir()
if (pagesDir === null) {
process.stdout.write('{}')
return
}
const pages = await listPageFiles(pagesDir)
if (pages.length === 0) {
process.stdout.write('{}')
return
}
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()
const collector = {
name: 'pest-tia-collector',
moduleParsed(info) {
const id = info.id
if (!id || id.startsWith('\0')) return
const deps = new Set()
for (const i of info.importedIds) if (i && !i.startsWith('\0')) deps.add(i)
for (const i of info.dynamicallyImportedIds) if (i && !i.startsWith('\0')) deps.add(i)
graph.set(id, deps)
},
}
const externalBare = {
name: 'pest-tia-external-bare',
resolveId(source) {
if (!source) return null
if (isLocalSpecifier(source, aliasKeys)) return null
return { id: source, external: true }
},
}
const assetStub = {
name: 'pest-tia-asset-stub',
load(id) {
if (!id) return null
if (ASSET_EXT_RE.test(id)) {
return { code: 'export default null', moduleSideEffects: false }
}
return null
},
}
const input = Object.create(null)
for (let i = 0; i < pages.length; i++) input[`p${i}`] = pages[i]
const bundle = await rolldown({
input,
cwd: PROJECT_ROOT,
resolve: {
alias,
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.mjs', '.cjs', '.json', '.vue', '.svelte'],
},
transform: { jsx: 'preserve' },
treeshake: false,
plugins: [externalBare, assetStub, collector],
logLevel: 'silent',
onLog: () => {},
})
try {
await bundle.generate({ format: 'esm' })
} finally {
await bundle.close()
}
const reverse = new Map()
const transitiveCache = new Map()
const computeTransitive = (id, stack) => {
const cached = transitiveCache.get(id)
if (cached) return cached
if (stack.has(id)) return null
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) {
if (!dep || dep.startsWith('\0')) continue
if (dep.startsWith(PROJECT_ROOT)) {
const rel = relative(PROJECT_ROOT, dep).split(sep).join('/')
acc.add(rel)
}
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)
if (complete) transitiveCache.set(id, acc)
return acc
}
for (const page of pages) {
const pageComponent = componentNameFor(page, pagesDir)
const reachable = computeTransitive(page, new Set())
if (!reachable) continue
for (const rel of reachable) {
const bucket = reverse.get(rel) ?? new Set()
bucket.add(pageComponent)
reverse.set(rel, bucket)
}
}
const payload = Object.create(null)
const keys = [...reverse.keys()].sort()
for (const key of keys) {
payload[key] = [...reverse.get(key)].sort()
}
process.stdout.write(JSON.stringify(payload))
}
const invokedDirectly = process.argv[1] !== undefined
&& import.meta.url === pathToFileURL(process.argv[1]).href
if (invokedDirectly) {
try {
await main()
} catch (err) {
process.stderr.write(String(err?.stack ?? err ?? 'unknown error'))
process.exit(1)
}
}
+19 -5
View File
@@ -6,6 +6,7 @@ use ParaTest\WrapperRunner\ApplicationForWrapperWorker;
use ParaTest\WrapperRunner\WrapperWorker; use ParaTest\WrapperRunner\WrapperWorker;
use Pest\Kernel; use Pest\Kernel;
use Pest\Plugins\Actions\CallsHandleArguments; use Pest\Plugins\Actions\CallsHandleArguments;
use Pest\Support\Container;
use Pest\TestSuite; use Pest\TestSuite;
use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
@@ -32,10 +33,13 @@ $bootPest = (static function (): void {
'status-file:', 'status-file:',
'progress-file:', 'progress-file:',
'unexpected-output-file:', 'unexpected-output-file:',
'testresult-file:', 'test-result-file:',
'result-cache-file:',
'teamcity-file:', 'teamcity-file:',
'testdox-file:', 'testdox-file:',
'testdox-color', 'testdox-color',
'testdox-columns:',
'testdox-summary',
'phpunit-argv:', 'phpunit-argv:',
]); ]);
@@ -55,13 +59,23 @@ $bootPest = (static function (): void {
} }
} }
$container = Container::getInstance();
$rootPath = dirname(PHPUNIT_COMPOSER_INSTALL, 2);
foreach (Kernel::RESTARTERS as $restarterClass) {
$restarter = $container->get($restarterClass);
$restarter->maybeRestart($rootPath, $_SERVER['argv']);
}
assert(isset($getopt['status-file']) && is_string($getopt['status-file'])); assert(isset($getopt['status-file']) && is_string($getopt['status-file']));
$statusFile = fopen($getopt['status-file'], 'wb'); $statusFile = fopen($getopt['status-file'], 'wb');
assert(is_resource($statusFile)); assert(is_resource($statusFile));
assert(isset($getopt['progress-file']) && is_string($getopt['progress-file'])); assert(isset($getopt['progress-file']) && is_string($getopt['progress-file']));
assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file'])); assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file']));
assert(isset($getopt['testresult-file']) && is_string($getopt['testresult-file'])); assert(isset($getopt['test-result-file']) && is_string($getopt['test-result-file']));
assert(! isset($getopt['result-cache-file']) || is_string($getopt['result-cache-file']));
assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file'])); assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file']));
assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file'])); assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file']));
@@ -77,11 +91,12 @@ $bootPest = (static function (): void {
$phpunitArgv, $phpunitArgv,
$getopt['progress-file'], $getopt['progress-file'],
$getopt['unexpected-output-file'], $getopt['unexpected-output-file'],
$getopt['testresult-file'], $getopt['test-result-file'],
$getopt['result-cache-file'] ?? null,
$getopt['teamcity-file'] ?? null, $getopt['teamcity-file'] ?? null,
$getopt['testdox-file'] ?? null, $getopt['testdox-file'] ?? null,
isset($getopt['testdox-color']), isset($getopt['testdox-color']),
$getopt['testdox-columns'] ?? null, (int) ($getopt['testdox-columns'] ?? null),
); );
while (true) { while (true) {
@@ -96,7 +111,6 @@ $bootPest = (static function (): void {
exit; exit;
} }
// It must be a 1 byte string to ensure filesize() is equal to the number of tests executed
$exitCode = $application->runTest(realpath(trim($testPath))); $exitCode = $application->runTest(realpath(trim($testPath)));
fwrite($statusFile, (string) $exitCode); fwrite($statusFile, (string) $exitCode);
+41 -23
View File
@@ -17,18 +17,21 @@
} }
], ],
"require": { "require": {
"php": "^8.2.0", "php": "^8.4",
"brianium/paratest": "^7.5.5", "brianium/paratest": "^7.24.0",
"nunomaduro/collision": "^8.4.0", "nunomaduro/collision": "^8.9.5",
"nunomaduro/termwind": "^2.1.0", "nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin": "^5.0.0",
"pestphp/pest-plugin-arch": "^3.0.0", "pestphp/pest-plugin-arch": "^5.0.0",
"pestphp/pest-plugin-mutate": "^3.0.5", "pestphp/pest-plugin-mutate": "^5.0.1",
"phpunit/phpunit": "^11.3.6" "pestphp/pest-plugin-profanity": "^5.0.0",
"phpunit/phpunit": "^13.3.0",
"symfony/process": "^8.1.0"
}, },
"conflict": { "conflict": {
"phpunit/phpunit": ">11.3.6", "filp/whoops": "<2.18.3",
"sebastian/exporter": "<6.0.0", "phpunit/phpunit": ">13.3.0",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0" "webmozart/assert": "<1.11.0"
}, },
"autoload": { "autoload": {
@@ -45,22 +48,30 @@
"Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers", "Tests\\Fixtures\\Covers\\": "tests/Fixtures/Covers",
"Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance", "Tests\\Fixtures\\Inheritance\\": "tests/Fixtures/Inheritance",
"Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch", "Tests\\Fixtures\\Arch\\": "tests/Fixtures/Arch",
"Tests\\Fixtures\\Tia\\": "tests/Fixtures/Tia",
"Tests\\": "tests/PHPUnit/" "Tests\\": "tests/PHPUnit/"
}, },
"classmap": [
"tests/Fixtures/Arch/ToBeCasedCorrectly/IncorrectCasing/incorrectCasing.php"
],
"files": [ "files": [
"tests/Autoload.php" "tests/Autoload.php"
] ]
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^3.0.0", "pestphp/pest-dev-tools": "^5.0.0",
"pestphp/pest-plugin-type-coverage": "^3.0.0", "pestphp/pest-plugin-browser": "^5.0.0",
"symfony/process": "^7.1.5" "pestphp/pest-plugin-phpstan": "^5.0.2",
"pestphp/pest-plugin-rector": "^5.0.3",
"pestphp/pest-plugin-type-coverage": "^5.0.2",
"psy/psysh": "^0.12.24"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
"config": { "config": {
"sort-packages": true, "sort-packages": true,
"preferred-install": "dist", "preferred-install": "dist",
"process-timeout": 0,
"allow-plugins": { "allow-plugins": {
"pestphp/pest-plugin": true "pestphp/pest-plugin": true
} }
@@ -69,19 +80,24 @@
"bin/pest" "bin/pest"
], ],
"scripts": { "scripts": {
"refacto": "rector", "lint": [
"lint": "pint", "rector",
"test:refacto": "rector --dry-run", "pint --parallel"
"test:lint": "pint --test", ],
"test:lint": [
"rector --dry-run",
"pint --parallel --test"
],
"test:profanity": "php bin/pest --profanity --compact",
"test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100", "test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", "test:unit": "php bin/pest --exclude-group=integration --compact",
"test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", "test:inline": "php bin/pest --configuration=phpunit.inline.xml",
"test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3", "test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3",
"test:integration": "php bin/pest --colors=always --group=integration -v", "test:integration": "php bin/pest --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", "test:tia": "php bin/pest --group=tia -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots --exclude-group=tia",
"test": [ "test": [
"@test:refacto",
"@test:lint", "@test:lint",
"@test:type:check", "@test:type:check",
"@test:type:coverage", "@test:type:coverage",
@@ -110,6 +126,8 @@
"Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Snapshot",
"Pest\\Plugins\\Verbose", "Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version", "Pest\\Plugins\\Version",
"Pest\\Plugins\\Shard",
"Pest\\Plugins\\Tia",
"Pest\\Plugins\\Parallel" "Pest\\Plugins\\Parallel"
] ]
}, },
-14
View File
@@ -1,14 +0,0 @@
version: "3.8"
services:
php:
build:
context: ./docker
volumes:
- .:/var/www/html
composer:
build:
context: ./docker
volumes:
- .:/var/www/html
entrypoint: ["composer"]
+4 -34
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -52,6 +20,8 @@ use PHPUnit\Util\Filter;
use PHPUnit\Util\ThrowableToStringMapper; use PHPUnit\Util\ThrowableToStringMapper;
/** /**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final readonly class ThrowableBuilder final readonly class ThrowableBuilder
@@ -68,7 +38,7 @@ final readonly class ThrowableBuilder
$previous = self::from($previous); $previous = self::from($previous);
} }
$trace = Filter::getFilteredStacktrace($t); $trace = Filter::stackTraceFromThrowableAsString($t);
if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) { if ($t instanceof RenderableOnCollisionEditor && $frame = $t->toCollisionEditor()) {
$file = $frame->getFile(); $file = $frame->getFile();
@@ -82,7 +52,7 @@ final readonly class ThrowableBuilder
$t->getMessage(), $t->getMessage(),
ThrowableToStringMapper::map($t), ThrowableToStringMapper::map($t),
$trace, $trace,
$previous $previous,
); );
} }
} }
+43 -34
View File
@@ -1,6 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/* /*
* This file is part of PHPUnit. * This file is part of PHPUnit.
* *
@@ -14,6 +15,9 @@ namespace PHPUnit\Logging\JUnit;
use DOMDocument; use DOMDocument;
use DOMElement; use DOMElement;
use Pest\Logging\Converter;
use Pest\Support\Container;
use Pest\TestSuite;
use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\Test;
use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\EventFacadeIsSealedException;
@@ -27,6 +31,7 @@ use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\PreparationStarted; use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PrintedUnexpectedOutput;
use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\TestSuite\Started;
use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Event\UnknownSubscriberTypeException;
@@ -41,13 +46,15 @@ use function str_replace;
use function trim; use function trim;
/** /**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class JunitXmlLogger final class JunitXmlLogger
{ {
private readonly Printer $printer; private readonly Printer $printer;
private readonly \Pest\Logging\Converter $converter; // pest-added private readonly Converter $converter;
private DOMDocument $document; private DOMDocument $document;
@@ -59,32 +66,32 @@ final class JunitXmlLogger
private array $testSuites = []; private array $testSuites = [];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteTests = [0]; private array $testSuiteTests = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteAssertions = [0]; private array $testSuiteAssertions = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteErrors = [0]; private array $testSuiteErrors = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteFailures = [0]; private array $testSuiteFailures = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteSkipped = [0]; private array $testSuiteSkipped = [0];
/** /**
* @psalm-var array<int,int> * @var array<int,int>
*/ */
private array $testSuiteTimes = [0]; private array $testSuiteTimes = [0];
@@ -105,15 +112,14 @@ final class JunitXmlLogger
public function __construct(Printer $printer, Facade $facade) public function __construct(Printer $printer, Facade $facade)
{ {
$this->printer = $printer; $this->printer = $printer;
$this->converter = new \Pest\Logging\Converter(\Pest\Support\Container::getInstance()->get(\Pest\TestSuite::class)->rootPath); // pest-added $this->converter = new Converter(Container::getInstance()->get(TestSuite::class)->rootPath);
$this->registerSubscribers($facade); $this->registerSubscribers($facade);
$this->createDocument(); $this->createDocument();
} }
public function flush(): void public function flush(): void
{ {
$this->printer->print($this->document->saveXML()); $this->printer->print($this->document->saveXML() ?: '');
$this->printer->flush(); $this->printer->flush();
} }
@@ -121,10 +127,9 @@ final class JunitXmlLogger
public function testSuiteStarted(Started $event): void public function testSuiteStarted(Started $event): void
{ {
$testSuite = $this->document->createElement('testsuite'); $testSuite = $this->document->createElement('testsuite');
$testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite())); // pest-changed $testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite()));
if ($event->testSuite()->isForTestClass()) { if ($event->testSuite()->isForTestClass()) {
$testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? ''); // pest-changed $testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? '');
} }
if ($this->testSuiteLevel > 0) { if ($this->testSuiteLevel > 0) {
@@ -195,28 +200,34 @@ final class JunitXmlLogger
$this->createTestCase($event); $this->createTestCase($event);
} }
/**
* @throws InvalidArgumentException
*/
public function testPreparationFailed(): void public function testPreparationFailed(): void
{ {
$this->preparationFailed = true; $this->preparationFailed = true;
} }
/**
* @throws InvalidArgumentException
*/
public function testPrepared(): void public function testPrepared(): void
{ {
$this->prepared = true; $this->prepared = true;
} }
public function testPrintedUnexpectedOutput(PrintedUnexpectedOutput $event): void
{
assert($this->currentTestCase !== null);
$systemOut = $this->document->createElement(
'system-out',
Xml::prepareString($event->output()),
);
$this->currentTestCase->appendChild($systemOut);
}
/** /**
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function testFinished(Finished $event): void public function testFinished(Finished $event): void
{ {
if ($this->preparationFailed) { if (! $this->prepared || $this->preparationFailed) {
return; return;
} }
@@ -305,9 +316,11 @@ final class JunitXmlLogger
new TestPreparationStartedSubscriber($this), new TestPreparationStartedSubscriber($this),
new TestPreparationFailedSubscriber($this), new TestPreparationFailedSubscriber($this),
new TestPreparedSubscriber($this), new TestPreparedSubscriber($this),
new TestPrintedUnexpectedOutputSubscriber($this),
new TestFinishedSubscriber($this), new TestFinishedSubscriber($this),
new TestErroredSubscriber($this), new TestErroredSubscriber($this),
new TestFailedSubscriber($this), new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this), new TestSkippedSubscriber($this),
new TestRunnerExecutionFinishedSubscriber($this), new TestRunnerExecutionFinishedSubscriber($this),
); );
@@ -333,12 +346,11 @@ final class JunitXmlLogger
assert($this->currentTestCase !== null); assert($this->currentTestCase !== null);
$buffer = $this->converter->getTestCaseMethodName($event->test()); // pest-changed $buffer = $this->converter->getTestCaseMethodName($event->test());
$throwable = $event->throwable(); $throwable = $event->throwable();
$buffer .= trim( $buffer .= trim(
$this->converter->getExceptionMessage($throwable).PHP_EOL. // pest-changed $this->converter->getExceptionMessage($throwable).PHP_EOL.
$this->converter->getExceptionDetails($throwable), // pest-changed $this->converter->getExceptionDetails($throwable),
); );
$fault = $this->document->createElement( $fault = $this->document->createElement(
@@ -431,25 +443,22 @@ final class JunitXmlLogger
/** /**
* @throws InvalidArgumentException * @throws InvalidArgumentException
* *
* @psalm-assert !null $this->currentTestCase * @phpstan-assert !null $this->currentTestCase
*/ */
private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void
{ {
$testCase = $this->document->createElement('testcase'); $testCase = $this->document->createElement('testcase');
$test = $event->test(); $test = $event->test();
$file = $this->converter->getTestCaseLocation($test); // pest-added $file = $this->converter->getTestCaseLocation($test);
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test));
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test)); // pest-changed $testCase->setAttribute('file', $file);
$testCase->setAttribute('file', $file); // pest-changed
if ($test->isTestMethod()) { if ($test->isTestMethod()) {
assert($test instanceof TestMethod); assert($test instanceof TestMethod);
//$testCase->setAttribute('line', (string) $test->line()); // pest-removed $className = $this->converter->getTrimmedTestClassName($test);
$className = $this->converter->getTrimmedTestClassName($test); // pest-added $testCase->setAttribute('class', $className);
$testCase->setAttribute('class', $className); // pest-changed $testCase->setAttribute('classname', str_replace('\\', '.', $className));
$testCase->setAttribute('classname', str_replace('\\', '.', $className)); // pest-changed
} }
$this->currentTestCase = $testCase; $this->currentTestCase = $testCase;
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace ParaTest\WrapperRunner;
use PHPUnit\TextUI\Output\Printer;
use function preg_match;
/**
* @internal
*/
final readonly class ProgressPrinterOutput implements Printer
{
public function __construct(
private Printer $progressPrinter,
private Printer $outputPrinter,
) {}
public function print(string $buffer): void
{
if (
$buffer === "\n"
|| preg_match('/^ +$/', $buffer) === 1
|| preg_match('/^ \d+ \/ \d+ \(...%\)$/', $buffer) === 1
) {
return;
}
match ($buffer) {
'E', 'F', 'I', 'N', 'D', 'R', 'W', 'S', 'T', '.' => $this->progressPrinter->print($buffer),
default => $this->outputPrinter->print($buffer),
};
}
public function flush(): void
{
$this->progressPrinter->flush();
$this->outputPrinter->flush();
}
}
+14 -114
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -48,42 +16,32 @@ namespace PHPUnit\Runner\Filter;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use PHPUnit\Framework\Test; use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\PhptTestCase; use PHPUnit\Runner\Phpt\TestCase as PhptTestCase;
use RecursiveFilterIterator; use RecursiveFilterIterator;
use RecursiveIterator; use RecursiveIterator;
use function end; use function end;
use function preg_match; use function preg_match;
use function sprintf; use function trim;
use function str_replace;
/** /**
* @extends RecursiveFilterIterator<int, Test, RecursiveIterator<int, Test>>
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
abstract class NameFilterIterator extends RecursiveFilterIterator abstract class NameFilterIterator extends RecursiveFilterIterator
{ {
/** private readonly CompiledNameFilter $filter;
* @psalm-var non-empty-string
*/
private readonly string $regularExpression;
private readonly ?int $dataSetMinimum;
private readonly ?int $dataSetMaximum;
/** /**
* @psalm-param RecursiveIterator<int, Test> $iterator * @param RecursiveIterator<int, Test> $iterator
* @psalm-param non-empty-string $filter * @param non-empty-string $filter
*/ */
public function __construct(RecursiveIterator $iterator, string $filter) public function __construct(RecursiveIterator $iterator, string $filter)
{ {
parent::__construct($iterator); parent::__construct($iterator);
$preparedFilter = $this->prepareFilter($filter); $this->filter = CompiledNameFilter::from($filter);
$this->regularExpression = $preparedFilter['regularExpression'];
$this->dataSetMinimum = $preparedFilter['dataSetMinimum'];
$this->dataSetMaximum = $preparedFilter['dataSetMaximum'];
} }
public function accept(): bool public function accept(): bool
@@ -99,80 +57,22 @@ abstract class NameFilterIterator extends RecursiveFilterIterator
} }
if ($test instanceof HasPrintableTestCaseName) { if ($test instanceof HasPrintableTestCaseName) {
$name = $test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName(); $name = trim(
$test::getPrintableTestCaseName().'::'.$test->getPrintableTestCaseMethodName().$test->dataSetAsString()
);
} else { } else {
$name = $test::class.'::'.$test->nameWithDataSet(); $name = $test::class.'::'.$test->nameWithDataSet();
} }
$accepted = @preg_match($this->regularExpression, $name, $matches) === 1; $accepted = @preg_match($this->filter->regularExpression(), $name, $matches) === 1;
if ($accepted && isset($this->dataSetMaximum)) { if ($accepted && $this->filter->hasDataSetRange()) {
$set = end($matches); $set = end($matches);
$accepted = $set >= $this->dataSetMinimum && $set <= $this->dataSetMaximum; $accepted = $set >= $this->filter->dataSetMinimum() && $set <= $this->filter->dataSetMaximum();
} }
return $this->doAccept($accepted); return $this->doAccept($accepted);
} }
abstract protected function doAccept(bool $result): bool; abstract protected function doAccept(bool $result): bool;
/**
* @psalm-param non-empty-string $filter
*
* @psalm-return array{regularExpression: non-empty-string, dataSetMinimum: ?int, dataSetMaximum: ?int}
*/
private function prepareFilter(string $filter): array
{
$dataSetMinimum = null;
$dataSetMaximum = null;
if (@preg_match($filter, '') === false) {
// Handles:
// * testAssertEqualsSucceeds#4
// * testAssertEqualsSucceeds#4-8
if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) {
if (isset($matches[3]) && $matches[2] < $matches[3]) {
$filter = sprintf(
'%s.*with data set #(\d+)$',
$matches[1],
);
$dataSetMinimum = (int) $matches[2];
$dataSetMaximum = (int) $matches[3];
} else {
$filter = sprintf(
'%s.*with data set #%s$',
$matches[1],
$matches[2],
);
}
} // Handles:
// * testDetermineJsonError@JSON_ERROR_NONE
// * testDetermineJsonError@JSON.*
elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) {
$filter = sprintf(
'%s.*with data set "%s"$',
$matches[1],
$matches[2],
);
}
// Escape delimiters in regular expression. Do NOT use preg_quote,
// to keep magic characters.
$filter = sprintf(
'/%s/i',
str_replace(
'/',
'\\/',
$filter,
),
);
}
return [
'regularExpression' => $filter,
'dataSetMinimum' => $dataSetMinimum,
'dataSetMaximum' => $dataSetMaximum,
];
}
} }
@@ -1,195 +0,0 @@
<?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Runner\ResultCache;
use const DIRECTORY_SEPARATOR;
use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\Runner\DirectoryCannotBeCreatedException;
use PHPUnit\Runner\Exception;
use PHPUnit\Util\Filesystem;
use function array_keys;
use function assert;
use function dirname;
use function file_get_contents;
use function file_put_contents;
use function is_array;
use function is_dir;
use function is_file;
use function json_decode;
use function json_encode;
use function Pest\version;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class DefaultResultCache implements ResultCache
{
/**
* @var string
*/
private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache';
private readonly string $cacheFilename;
/**
* @psalm-var array<string, TestStatus>
*/
private array $defects = [];
/**
* @psalm-var array<string, float>
*/
private array $times = [];
public function __construct(?string $filepath = null)
{
if ($filepath !== null && is_dir($filepath)) {
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME;
}
$this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME;
}
public function setStatus(string $id, TestStatus $status): void
{
if ($status->isSuccess()) {
return;
}
$this->defects[$id] = $status;
}
public function status(string $id): TestStatus
{
return $this->defects[$id] ?? TestStatus::unknown();
}
public function setTime(string $id, float $time): void
{
$this->times[$id] = $time;
}
public function time(string $id): float
{
return $this->times[$id] ?? 0.0;
}
public function load(): void
{
if (! is_file($this->cacheFilename)) {
return;
}
$contents = file_get_contents($this->cacheFilename);
if ($contents === false) {
return;
}
$data = json_decode(
$contents,
true,
);
if ($data === null) {
return;
}
if (! isset($data['version'])) {
return;
}
if ($data['version'] !== $this->cacheVersion()) {
return;
}
assert(isset($data['defects']) && is_array($data['defects']));
assert(isset($data['times']) && is_array($data['times']));
foreach (array_keys($data['defects']) as $test) {
$data['defects'][$test] = TestStatus::from($data['defects'][$test]);
}
$this->defects = $data['defects'];
$this->times = $data['times'];
}
/**
* @throws Exception
*/
public function persist(): void
{
if (! Filesystem::createDirectory(dirname($this->cacheFilename))) {
throw new DirectoryCannotBeCreatedException($this->cacheFilename);
}
$data = [
'version' => $this->cacheVersion(),
'defects' => [],
'times' => $this->times,
];
foreach ($this->defects as $test => $status) {
$data['defects'][$test] = $status->asInt();
}
file_put_contents(
$this->cacheFilename,
json_encode($data),
LOCK_EX,
);
}
/**
* Returns the cache version.
*/
private function cacheVersion(): string
{
return 'pest_'.version();
}
}
@@ -0,0 +1,302 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Runner\TestRunHistory;
use const DIRECTORY_SEPARATOR;
use const LOCK_EX;
use const LOCK_UN;
use PHPUnit\Framework\TestStatus\TestStatus;
use PHPUnit\Runner\DirectoryDoesNotExistException;
use PHPUnit\Runner\Exception;
use PHPUnit\Util\Filesystem;
use function array_keys;
use function dirname;
use function fclose;
use function file_get_contents;
use function flock;
use function fopen;
use function ftruncate;
use function fwrite;
use function is_array;
use function is_dir;
use function is_file;
use function is_float;
use function is_int;
use function is_string;
use function json_decode;
use function json_encode;
use function Pest\version;
use function rewind;
use function stream_get_contents;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class DefaultTestRunHistory implements TestRunHistory
{
private const string DEFAULT_FILENAME = '.phpunit.result.cache';
private readonly string $filename;
/**
* @var array<string, TestStatus>
*/
private array $defects = [];
/**
* @var array<string, float>
*/
private array $times = [];
/**
* @var array<string, true>
*/
private array $changedDefects = [];
/**
* @var array<string, true>
*/
private array $changedTimes = [];
public function __construct(?string $filepath = null)
{
if ($filepath !== null && is_dir($filepath)) {
$filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_FILENAME;
}
$this->filename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_FILENAME;
}
public function setStatus(TestRunHistoryId $id, TestStatus $status): void
{
if ($status->isSuccess()) {
return;
}
$this->defects[$id->asString()] = $status;
$this->changedDefects[$id->asString()] = true;
}
public function remove(TestRunHistoryId $id): void
{
unset($this->defects[$id->asString()]);
$this->changedDefects[$id->asString()] = true;
}
public function status(TestRunHistoryId $id): TestStatus
{
return $this->defects[$id->asString()] ?? TestStatus::unknown();
}
public function setTime(TestRunHistoryId $id, float $time): void
{
$this->times[$id->asString()] = $time;
$this->changedTimes[$id->asString()] = true;
}
public function time(TestRunHistoryId $id): float
{
return $this->times[$id->asString()] ?? 0.0;
}
public function mergeWith(self $other): void
{
foreach ($other->defects as $id => $defect) {
$this->defects[$id] = $defect;
$this->changedDefects[$id] = true;
}
foreach ($other->times as $id => $time) {
$this->times[$id] = $time;
$this->changedTimes[$id] = true;
}
}
public function load(): void
{
if (! is_file($this->filename)) {
return;
}
$contents = file_get_contents($this->filename);
if ($contents === false) {
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}
$parsed = $this->parse($contents);
if ($parsed === null) {
return;
}
[$this->defects, $this->times] = $parsed;
$this->changedDefects = [];
$this->changedTimes = [];
}
/**
* @throws Exception
*/
public function persist(): void
{
$this->writeToFile(false);
}
/**
* @throws Exception
*/
public function persistAndPrune(): void
{
$this->writeToFile(true);
}
/**
* @throws Exception
*/
private function writeToFile(bool $prune): void
{
if (! Filesystem::createDirectory(dirname($this->filename))) {
throw new DirectoryDoesNotExistException(dirname($this->filename));
}
$handle = fopen($this->filename, 'c+');
if ($handle === false) {
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}
flock($handle, LOCK_EX);
if ($prune) {
$defects = [];
foreach ($this->defects as $id => $status) {
if (isset($this->changedDefects[$id])) {
$defects[$id] = $status;
}
}
$times = [];
foreach ($this->times as $id => $time) {
if (isset($this->changedTimes[$id]) || isset($this->changedDefects[$id])) {
$times[$id] = $time;
}
}
} else {
$parsed = $this->parse((string) stream_get_contents($handle));
if ($parsed !== null) {
[$defects, $times] = $parsed;
foreach (array_keys($this->changedDefects) as $id) {
if (isset($this->defects[$id])) {
$defects[$id] = $this->defects[$id];
} else {
unset($defects[$id]);
}
}
foreach ($this->times as $id => $time) {
if (isset($this->changedTimes[$id])) {
$times[$id] = $time;
}
}
} else {
$defects = $this->defects;
$times = $this->times;
}
}
$data = [
'version' => $this->version(),
'defects' => [],
'times' => $times,
];
foreach ($defects as $test => $status) {
$data['defects'][$test] = $status->asInt();
}
$json = json_encode($data);
if ($json !== false) {
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, $json);
}
flock($handle, LOCK_UN);
fclose($handle);
}
/**
* @return ?array{0: array<string, TestStatus>, 1: array<string, float>}
*/
private function parse(string $contents): ?array
{
$data = json_decode(
$contents,
true,
);
if (! is_array($data)) {
return null;
}
if (! isset($data['version']) || $data['version'] !== $this->version()) {
return null;
}
if (! isset($data['defects'], $data['times']) || ! is_array($data['defects']) || ! is_array($data['times'])) {
return null;
}
$defects = [];
foreach ($data['defects'] as $test => $status) {
if (! is_string($test) || ! is_int($status)) {
continue;
}
$defects[$test] = TestStatus::from($status);
}
$times = [];
foreach ($data['times'] as $test => $time) {
if (! is_string($test) || (! is_float($time) && ! is_int($time))) {
continue;
}
$times[$test] = (float) $time;
}
return [$defects, $times];
}
private function version(): string
{
return 'pest_'.version();
}
}
+2 -34
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
namespace PHPUnit\Runner; namespace PHPUnit\Runner;
@@ -90,11 +58,11 @@ final class TestSuiteLoader
(static function () use ($suiteClassFile) { (static function () use ($suiteClassFile) {
try { try {
include_once $suiteClassFile; include_once $suiteClassFile;
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
} catch (Throwable $e) { } catch (Throwable $e) {
Panic::with($e); Panic::with($e);
} }
TestSuite::getInstance()->tests->makeIfNeeded($suiteClassFile);
})(); })();
$loadedClasses = array_values( $loadedClasses = array_values(
+389
View File
@@ -0,0 +1,389 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Runner;
use PHPUnit\Framework\DataProviderTestSuite;
use PHPUnit\Framework\IterativeTestSuite;
use PHPUnit\Framework\Reorderable;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\TestRunHistory\NullTestRunHistory;
use PHPUnit\Runner\TestRunHistory\TestRunHistory;
use PHPUnit\Runner\TestRunHistory\TestRunHistoryId;
use function array_diff;
use function array_merge;
use function array_reverse;
use function array_splice;
use function assert;
use function count;
use function in_array;
use function max;
use function shuffle;
use function usort;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class TestSuiteSorter
{
public const int ORDER_DEFAULT = 0;
public const int ORDER_RANDOMIZED = 1;
public const int ORDER_REVERSED = 2;
public const int ORDER_DEFECTS_FIRST = 3;
public const int ORDER_DURATION_ASCENDING = 4;
public const int ORDER_SIZE_ASCENDING = 5;
public const int ORDER_DURATION_DESCENDING = 6;
public const int ORDER_SIZE_DESCENDING = 7;
/**
* @var non-empty-array<non-empty-string, positive-int>
*/
private const array SIZE_SORT_WEIGHT = [
'small' => 1,
'medium' => 2,
'large' => 3,
'unknown' => 4,
];
/**
* @var array<string, int> Associative array of (string => DEFECT_SORT_WEIGHT) elements
*/
private array $defectSortOrder = [];
private readonly TestRunHistory $testRunHistory;
public function __construct(?TestRunHistory $testRunHistory = null)
{
$this->testRunHistory = $testRunHistory ?? new NullTestRunHistory;
}
/**
* @throws Exception
*/
public function reorderTestsInSuite(Test $suite, int $order, bool $resolveDependencies, int $orderDefects): void
{
$allowedOrders = [
self::ORDER_DEFAULT,
self::ORDER_REVERSED,
self::ORDER_RANDOMIZED,
self::ORDER_DURATION_ASCENDING,
self::ORDER_SIZE_ASCENDING,
self::ORDER_DURATION_DESCENDING,
self::ORDER_SIZE_DESCENDING,
];
if (! in_array($order, $allowedOrders, true)) {
// @codeCoverageIgnoreStart
throw new InvalidOrderException;
// @codeCoverageIgnoreEnd
}
$allowedOrderDefects = [
self::ORDER_DEFAULT,
self::ORDER_DEFECTS_FIRST,
];
if (! in_array($orderDefects, $allowedOrderDefects, true)) {
// @codeCoverageIgnoreStart
throw new InvalidOrderException;
// @codeCoverageIgnoreEnd
}
if ($suite instanceof IterativeTestSuite) {
return;
}
if ($suite instanceof TestSuite) {
foreach ($suite as $_suite) {
$this->reorderTestsInSuite($_suite, $order, $resolveDependencies, $orderDefects);
}
if ($orderDefects === self::ORDER_DEFECTS_FIRST) {
$this->addSuiteToDefectSortOrder($suite);
}
$this->sort($suite, $order, $resolveDependencies, $orderDefects);
}
}
private function sort(TestSuite $suite, int $order, bool $resolveDependencies, int $orderDefects): void
{
$tests = $suite->tests();
if ($tests === []) {
return;
}
if ($order === self::ORDER_REVERSED) {
$tests = $this->reverse($tests);
} elseif ($order === self::ORDER_RANDOMIZED) {
$tests = $this->randomize($tests);
} elseif ($order === self::ORDER_DURATION_ASCENDING) {
$tests = $this->sortByDuration($tests);
} elseif ($order === self::ORDER_DURATION_DESCENDING) {
$tests = $this->sortByDurationDescending($tests);
} elseif ($order === self::ORDER_SIZE_ASCENDING) {
$tests = $this->sortBySize($tests);
} elseif ($order === self::ORDER_SIZE_DESCENDING) {
$tests = $this->sortBySizeDescending($tests);
}
if ($orderDefects === self::ORDER_DEFECTS_FIRST) {
$tests = $this->sortDefectsFirst($tests);
}
if ($resolveDependencies && ! ($suite instanceof DataProviderTestSuite)) {
/** @noinspection PhpParamsInspection */
/** @phpstan-ignore argument.type */
$tests = $this->resolveDependencies($tests);
}
$suite->setTests($tests);
}
private function addSuiteToDefectSortOrder(TestSuite $suite): void
{
$max = 0;
foreach ($suite->tests() as $test) {
assert($test instanceof Reorderable);
$sortId = $test->sortId();
if (! isset($this->defectSortOrder[$sortId])) {
$this->defectSortOrder[$sortId] = $this->testRunHistory->status(TestRunHistoryId::fromReorderable($test))->sortWeight();
}
$max = max($max, $this->defectSortOrder[$sortId]);
}
$this->defectSortOrder[$suite->sortId()] = $max;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function reverse(array $tests): array
{
return array_reverse($tests);
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function randomize(array $tests): array
{
shuffle($tests);
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function sortDefectsFirst(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpDefectPriorityAndTime($left, $right),
);
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function sortByDuration(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpDuration($left, $right),
);
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function sortByDurationDescending(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpDuration($right, $left),
);
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function sortBySize(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpSize($left, $right),
);
return $tests;
}
/**
* @param list<Test> $tests
* @return list<Test>
*/
private function sortBySizeDescending(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpSize($right, $left),
);
return $tests;
}
private function cmpDefectPriorityAndTime(Test $a, Test $b): int
{
assert($a instanceof Reorderable);
assert($b instanceof Reorderable);
$priorityA = $this->defectSortOrder[$a->sortId()] ?? 0;
$priorityB = $this->defectSortOrder[$b->sortId()] ?? 0;
return $priorityB <=> $priorityA;
}
private function cmpDuration(Test $a, Test $b): int
{
return $this->durationWeight($a) <=> $this->durationWeight($b);
}
private function durationWeight(Test $test): float
{
if ($test instanceof TestSuite) {
$sum = 0.0;
foreach ($test->tests() as $inner) {
$sum += $this->durationWeight($inner);
}
return $sum;
}
if ($test instanceof Reorderable) {
return $this->testRunHistory->time(TestRunHistoryId::fromReorderable($test));
}
return 0.0;
}
private function cmpSize(Test $a, Test $b): int
{
return $this->sizeWeight($a) <=> $this->sizeWeight($b);
}
/**
* @return positive-int
*/
private function sizeWeight(Test $test): int
{
if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) {
return self::SIZE_SORT_WEIGHT[$test->size()->asString()];
}
if ($test instanceof TestSuite) {
$max = 0;
foreach ($test->tests() as $inner) {
$weight = $this->sizeWeight($inner);
if ($weight > $max) {
$max = $weight;
}
}
if ($max > 0) {
return $max;
}
}
return self::SIZE_SORT_WEIGHT['unknown'];
}
/**
* @param list<TestCase> $tests
* @return list<TestCase>
*/
private function resolveDependencies(array $tests): array
{
if (! $this->anyTestHasDependencies($tests)) {
return $tests;
}
$newTestOrder = [];
$i = 0;
$provided = [];
while ($tests !== [] && $i < count($tests)) {
if (array_diff($tests[$i]->requires(), $provided) === []) {
$provided = array_merge($provided, $tests[$i]->provides());
$newTestOrder = array_merge($newTestOrder, array_splice($tests, $i, 1));
$i = 0;
} else {
$i++;
}
}
return array_merge($newTestOrder, $tests);
}
/**
* @param iterable<Test> $tests
*/
private function anyTestHasDependencies(iterable $tests): bool
{
foreach ($tests as $test) {
if ($test instanceof TestSuite) {
if ($this->anyTestHasDependencies($test->tests())) {
return true;
}
continue;
}
if ($test instanceof TestCase && $test->requires() !== []) {
return true;
}
}
return false;
}
}
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
@@ -43,7 +11,7 @@ declare(strict_types=1);
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace Pest\Logging\TeamCity\Subscriber; namespace PHPUnit\TextUI\Output\Default\ProgressPrinter;
use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber; use PHPUnit\Event\Test\SkippedSubscriber;
@@ -52,24 +20,21 @@ use ReflectionClass;
/** /**
* @internal This class is not covered by the backward compatibility promise for PHPUnit * @internal This class is not covered by the backward compatibility promise for PHPUnit
*/ */
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber final readonly class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{ {
public function notify(Skipped $event): void public function notify(Skipped $event): void
{ {
if (str_contains($event->message(), '__TODO__')) { if ($event->message() === '__TODO__') {
$this->printTodoItem(); $this->printTodoItem();
} }
$this->logger()->testSkipped($event); $this->printer()->testSkipped();
} }
/**
* Prints a "T" to the standard PHPUnit output to indicate a todo item.
*/
private function printTodoItem(): void private function printTodoItem(): void
{ {
$mirror = new ReflectionClass($this->printer()); $mirror = new ReflectionClass($this->printer());
$printerMirror = $mirror->getMethod('printProgress'); $printProgress = $mirror->getMethod('printProgress');
$printerMirror->invoke($this->printer(), 'T'); $printProgress->invoke($this->printer(), 'T');
} }
} }
+9 -32
View File
@@ -1,37 +1,5 @@
<?php <?php
/*
* BSD 3-Clause License
*
* Copyright (c) 2001-2023, Sebastian Bergmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
declare(strict_types=1); declare(strict_types=1);
/* /*
* This file is part of PHPUnit. * This file is part of PHPUnit.
@@ -45,6 +13,7 @@ declare(strict_types=1);
namespace PHPUnit\TextUI; namespace PHPUnit\TextUI;
use Pest\Plugins\Only; use Pest\Plugins\Only;
use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter;
use PHPUnit\Event; use PHPUnit\Event;
use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestSuite;
use PHPUnit\Runner\Filter\Factory; use PHPUnit\Runner\Filter\Factory;
@@ -66,6 +35,12 @@ final readonly class TestSuiteFilterProcessor
{ {
$factory = new Factory; $factory = new Factory;
// @phpstan-ignore-next-line
(fn () => $this->filters[] = [
'className' => EnsureTestCaseIsInitiatedFilter::class,
'argument' => '',
])->call($factory);
if (! $configuration->hasFilter() && if (! $configuration->hasFilter() &&
! $configuration->hasGroups() && ! $configuration->hasGroups() &&
! $configuration->hasExcludeGroups() && ! $configuration->hasExcludeGroups() &&
@@ -73,6 +48,8 @@ final readonly class TestSuiteFilterProcessor
! $configuration->hasTestsCovering() && ! $configuration->hasTestsCovering() &&
! $configuration->hasTestsUsing() && ! $configuration->hasTestsUsing() &&
! Only::isEnabled()) { ! Only::isEnabled()) {
$suite->injectFilter($factory);
return; return;
} }
+121
View File
@@ -0,0 +1,121 @@
parameters:
ignoreErrors:
-
message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: src/Concerns/Expectable.php
-
message: '#^Trait Pest\\Concerns\\Logging\\WritesToConsole is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: src/Concerns/Logging/WritesToConsole.php
-
message: '#^Trait Pest\\Concerns\\Testable is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: src/Concerns/Testable.php
-
message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\<TAndValue\> but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\<TAndValue of mixed\>\.$#'
identifier: return.type
count: 1
path: src/Expectation.php
-
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$each contains generic class Pest\\Expectations\\EachExpectation but does not specify its types\: TValue$#'
identifier: missingType.generics
count: 1
path: src/Expectation.php
-
message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$not contains generic class Pest\\Expectations\\OppositeExpectation but does not specify its types\: TValue$#'
identifier: missingType.generics
count: 1
path: src/Expectation.php
-
message: '#^Parameter \#2 \$newScope of method Closure\:\:bindTo\(\) expects ''static''\|class\-string\|object\|null, string given\.$#'
identifier: argument.type
count: 1
path: src/Expectation.php
-
message: '#^Function expect\(\) should return Pest\\Expectation\<TValue\|null\> but returns Pest\\Expectation\<TValue\|null\>\.$#'
identifier: return.type
count: 1
path: src/Functions.php
-
message: '#^Parameter \#1 \$argv of method PHPUnit\\TextUI\\Application\:\:run\(\) expects list\<string\>, array\<int, string\> given\.$#'
identifier: argument.type
count: 1
path: src/Kernel.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:__toString\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Mixins/Expectation.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:toArray\(\)\.$#'
identifier: method.notFound
count: 4
path: src/Mixins/Expectation.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:toSnapshot\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Mixins/Expectation.php
-
message: '#^Call to an undefined method object&TValue of mixed\:\:toString\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Mixins/Expectation.php
-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
identifier: staticMethod.alreadyNarrowedType
count: 2
path: src/Mixins/Expectation.php
-
message: '#^PHPDoc tag @var with type callable\(\)\: bool is not subtype of native type Closure\|null\.$#'
identifier: varTag.nativeType
count: 1
path: src/PendingCalls/TestCall.php
-
message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\AfterLastTestMethodErrored\|PHPUnit\\Event\\Test\\BeforeFirstTestMethodErrored\|PHPUnit\\Event\\Test\\Errored\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestSuite\\Skipped\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#8 \$testSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\Skipped\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#9 \$testMarkedIncompleteEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\MarkedIncomplete\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Property Pest\\Plugins\\Parallel\\Paratest\\WrapperRunner\:\:\$pending \(list\<non\-empty\-string\>\) does not accept array\<int, non\-empty\-string\>\.$#'
identifier: assign.propertyType
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
+5
View File
@@ -0,0 +1,5 @@
services:
-
class: Pest\PHPStan\HigherOrderExpectationTypeExtension
tags:
- phpstan.broker.expressionTypeResolverExtension
+5 -17
View File
@@ -1,23 +1,11 @@
includes: includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon - phpstan-baseline.neon
- vendor/ergebnis/phpstan-rules/rules.neon - phpstan-pest-extension.neon
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon - vendor/pestphp/pest-plugin-phpstan/extension.neon
parameters: parameters:
level: max level: 7
paths: paths:
- src - src
checkMissingIterableValueType: true reportUnmatchedIgnoredErrors: false
reportUnmatchedIgnoredErrors: true
ignoreErrors:
- "#has a nullable return type declaration.#"
- "#Language construct isset\\(\\) should not be used.#"
- "#is not allowed to extend#"
- "#is concrete, but does not have a Test suffix#"
- "#with a nullable type declaration#"
- "#type mixed is not subtype of native#"
- "# with null as default value#"
- "#has parameter \\$closure with default value.#"
- "#has parameter \\$description with default value.#"
+3 -1
View File
@@ -16,9 +16,11 @@
<testsuites> <testsuites>
<testsuite name="default"> <testsuite name="default">
<directory suffix=".php">./tests</directory> <directory suffix=".php">./tests</directory>
<directory suffix=".php">./tests-external</directory>
<exclude>./tests/.snapshots</exclude> <exclude>./tests/.snapshots</exclude>
<exclude>./tests/.tests</exclude>
<exclude>./tests/Fixtures/Inheritance</exclude> <exclude>./tests/Fixtures/Inheritance</exclude>
<exclude>./tests/Fixtures/Suites</exclude>
<exclude>./tests/Fixtures/Tia</exclude>
</testsuite> </testsuite>
</testsuites> </testsuites>
<source> <source>
+8
View File
@@ -0,0 +1,8 @@
{
"exclude": [
"tests/Fixtures/Suites"
],
"rules": {
"Pint/phpdoc_type_annotations_only": true
}
}
+39
View File
@@ -2,16 +2,55 @@
declare(strict_types=1); declare(strict_types=1);
use Pest\Rector\Rules\UseToMatchArrayRector;
use Pest\Rector\Set\PestSetList;
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
use Rector\Config\RectorConfig; use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveParentDelegatingConstructorRector;
use Rector\DeadCode\Rector\ClassMethod\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; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return RectorConfig::configure() return RectorConfig::configure()
->withPaths([ ->withPaths([
__DIR__.'/src', __DIR__.'/src',
__DIR__.'/tests',
])
->withSets([
PestSetList::CODING_STYLE,
]) ])
->withSkip([ ->withSkip([
__DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php', __DIR__.'/src/Plugins/Parallel/Paratest/WrapperRunner.php',
__DIR__.'/tests/Fixtures/Arch',
__DIR__.'/tests/Fixtures/Suites',
ReturnNeverTypeRector::class, ReturnNeverTypeRector::class,
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
NarrowObjectReturnTypeRector::class,
RemoveParentDelegatingConstructorRector::class,
RemoveDuplicatedReturnSelfDocblockRector::class,
RemoveUselessUnionReturnDocblockRector::class,
RemoveReturnTagIncompatibleWithNativeTypeRector::class => [
__DIR__.'/src/Expectations/HigherOrderExpectation.php',
],
UseToMatchArrayRector::class,
RemoveEmptyClassMethodRector::class => [
__DIR__.'/tests',
],
RemoveUnusedConstructorParamRector::class => [
__DIR__.'/tests',
],
RemoveUnusedPrivatePropertyRector::class => [
__DIR__.'/tests',
],
AddArrowFunctionReturnTypeRector::class => [
__DIR__.'/tests',
],
]) ])
->withPreparedSets( ->withPreparedSets(
deadCode: true, deadCode: true,
@@ -0,0 +1,196 @@
---
name: pest-testing
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, architecture tests, or faster test runs with Test Impact Analysis. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, Tia (--tia), sharding, and all Pest 5 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
author: laravel
---
@php
/** @var \Laravel\Boost\Install\GuidelineAssist $assist */
@endphp
# Pest Testing 5
## Documentation
Use `search-docs` for detailed Pest 5 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `{{ $assist->artisanCommand('make:test --pest {name}') }}`.
The `{name}` argument should include only the path and test name, but should not include the test suite.
- Incorrect: `{{ $assist->artisanCommand('make:test --pest Feature/SomeFeatureTest') }}` will generate `tests/Feature/Feature/SomeFeatureTest.php`
- Correct: `{{ $assist->artisanCommand('make:test --pest SomeControllerTest') }}` will generate `tests/Feature/SomeControllerTest.php`
- Incorrect: `{{ $assist->artisanCommand('make:test --pest --unit Unit/SomeServiceTest') }}` will generate `tests/Unit/Unit/SomeServiceTest.php`
- Correct: `{{ $assist->artisanCommand('make:test --pest --unit SomeServiceTest') }}` will generate `tests/Unit/SomeServiceTest.php`
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
@boostsnippet("Basic Pest Test Example", "php")
it('is true', function () {
expect(true)->toBeTrue();
});
@endboostsnippet
### Running Tests
- Run minimal tests with filter before finalizing: `{{ $assist->artisanCommand('test --compact --filter=testName') }}`.
- Run all tests: `{{ $assist->artisanCommand('test --compact') }}`.
- Run file: `{{ $assist->artisanCommand('test --compact tests/Feature/ExampleTest.php') }}`.
- Run only tests affected by recent changes (Tia): `./vendor/bin/pest --parallel --tia`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
@boostsnippet("Pest Response Assertion", "php")
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
@endboostsnippet
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
@boostsnippet("Pest Dataset Example", "php")
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
@endboostsnippet
## Pest 5 Features
| Feature | Purpose |
|---------|---------|
| Tia (Test Impact Analysis) | Rerun only tests affected by recent changes |
| Time-Balanced Sharding | Split tests across CI shards by execution time |
| New Validation Expectations | `toBeEmail()`, `toBeUlid()`, `toBeIpAddress()`, and more |
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Architecture Testing | Enforce code conventions |
### Tia (Test Impact Analysis)
Tia reruns only tests affected by recent changes and replays cached results for the rest, dramatically reducing suite duration:
@boostsnippet("Tia Example", "shell")
./vendor/bin/pest --parallel --tia
@endboostsnippet
- Replayed tests are not skipped — cached tests store everything they produced, including covered lines and branches.
- Detects Laravel, Symfony, Livewire, and Inertia automatically.
### New Validation Expectations
Pest 5 ships eight new validation matchers, all supporting `.not` negation:
@boostsnippet("Pest 5 Validation Expectations", "php")
expect('nuno@pestphp.com')->toBeEmail();
expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid();
expect('192.168.1.1')->toBeIpAddress();
expect('00:1a:2b:3c:4d:5e')->toBeMacAddress();
expect('example.com')->toBeHostname();
expect('example.co.uk')->toBeDomain();
expect('Zm9vYmFy')->toBeBase64();
expect('deadbeef')->toBeHexadecimal();
@endboostsnippet
### Time-Balanced Sharding
Distribute tests across CI shards by execution time rather than count:
@boostsnippet("Pest Sharding Example", "shell")
./vendor/bin/pest --update-shards
./vendor/bin/pest --shard=1/4
@endboostsnippet
Commit `tests/.pest/shards.json` to the repository so CI shards stay consistent.
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
@boostsnippet("Pest Browser Test Example", "php")
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
@endboostsnippet
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
@boostsnippet("Pest Smoke Testing Example", "php")
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
@endboostsnippet
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Architecture Testing
@boostsnippet("Architecture Test Example", "php")
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
@endboostsnippet
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
+2
View File
@@ -5,6 +5,8 @@
[$bgBadgeColor, $bgBadgeText] = match ($type) { [$bgBadgeColor, $bgBadgeText] = match ($type) {
'INFO' => ['blue', 'INFO'], 'INFO' => ['blue', 'INFO'],
'ERROR' => ['red', 'ERROR'], 'ERROR' => ['red', 'ERROR'],
'WARN' => ['yellow', 'WARN'],
'SUCCESS' => ['green', 'SUCCESS'],
}; };
?> ?>
@@ -0,0 +1,22 @@
<div class="mx-2 mb-1">
<p>
<span>Using the <span class="text-yellow font-bold">visit()</span> function requires the Pest Plugin Browser to be installed.</span>
<span class="ml-1 text-yellow font-bold">Run:</span>
</p>
<div>
<span class="text-gray mr-1">- </span>
<span>composer require pestphp/pest-plugin-browser:^5.0 --dev</span>
</div>
<div>
<span class="text-gray mr-1">- </span>
<span>npm install playwright@latest</span>
</div>
<div>
<span class="text-gray mr-1">- </span>
<span>npx playwright install</span>
</div>
</div>
+1 -14
View File
@@ -13,15 +13,11 @@ use Pest\Expectation;
abstract class AbstractPreset // @pest-arch-ignore-line abstract class AbstractPreset // @pest-arch-ignore-line
{ {
/** /**
* The expectations.
*
* @var array<int, Expectation<mixed>|ArchExpectation> * @var array<int, Expectation<mixed>|ArchExpectation>
*/ */
protected array $expectations = []; protected array $expectations = [];
/** /**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces * @param array<int, string> $userNamespaces
*/ */
public function __construct( public function __construct(
@@ -31,15 +27,11 @@ abstract class AbstractPreset // @pest-arch-ignore-line
} }
/** /**
* Executes the arch preset.
*
* @internal * @internal
*/ */
abstract public function execute(): void; abstract public function execute(): void;
/** /**
* Ignores the given "targets" or "dependencies".
*
* @param array<int, string>|string $targetsOrDependencies * @param array<int, string>|string $targetsOrDependencies
*/ */
final public function ignoring(array|string $targetsOrDependencies): void final public function ignoring(array|string $targetsOrDependencies): void
@@ -51,9 +43,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line
} }
/** /**
* Runs the given callback for each namespace. * @param callable(Expectation<string>): ArchExpectation ...$callbacks
*
* @param callable(Expectation<string|null>): ArchExpectation ...$callbacks
*/ */
final public function eachUserNamespace(callable ...$callbacks): void final public function eachUserNamespace(callable ...$callbacks): void
{ {
@@ -64,9 +54,6 @@ abstract class AbstractPreset // @pest-arch-ignore-line
} }
} }
/**
* Flushes the expectations.
*/
final public function flush(): void final public function flush(): void
{ {
$this->expectations = []; $this->expectations = [];
-8
View File
@@ -14,8 +14,6 @@ use Pest\Expectation;
final class Custom extends AbstractPreset final class Custom extends AbstractPreset
{ {
/** /**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces * @param array<int, string> $userNamespaces
* @param Closure(array<int, string>): array<Expectation<mixed>|ArchExpectation> $execute * @param Closure(array<int, string>): array<Expectation<mixed>|ArchExpectation> $execute
*/ */
@@ -27,17 +25,11 @@ final class Custom extends AbstractPreset
parent::__construct($userNamespaces); parent::__construct($userNamespaces);
} }
/**
* Returns the name of the preset.
*/
public function name(): string public function name(): string
{ {
return $this->name; return $this->name;
} }
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations = ($this->execute)($this->userNamespaces); $this->expectations = ($this->execute)($this->userNamespaces);
+23 -9
View File
@@ -11,9 +11,6 @@ use Throwable;
*/ */
final class Laravel extends AbstractPreset final class Laravel extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations[] = expect('App\Traits') $this->expectations[] = expect('App\Traits')
@@ -27,17 +24,21 @@ final class Laravel extends AbstractPreset
->ignoring('App\Enums'); ->ignoring('App\Enums');
$this->expectations[] = expect('App\Enums') $this->expectations[] = expect('App\Enums')
->toBeEnums(); ->toBeEnums()
->ignoring('App\Enums\Concerns');
$this->expectations[] = expect('App\Features') $this->expectations[] = expect('App\Features')
->toBeClasses(); ->toBeClasses()
->ignoring('App\Features\Concerns');
$this->expectations[] = expect('App\Features') $this->expectations[] = expect('App\Features')
->toHaveMethod('resolve'); ->toHaveMethod('resolve')
->ignoring('App\Features\Concerns');
$this->expectations[] = expect('App\Exceptions') $this->expectations[] = expect('App\Exceptions')
->classes() ->classes()
->toImplement('Throwable'); ->toImplement('Throwable')
->ignoring('App\Exceptions\Handler');
$this->expectations[] = expect('App') $this->expectations[] = expect('App')
->not->toImplement(Throwable::class) ->not->toImplement(Throwable::class)
@@ -65,6 +66,7 @@ final class Laravel extends AbstractPreset
->toHaveSuffix('Request'); ->toHaveSuffix('Request');
$this->expectations[] = expect('App\Http\Requests') $this->expectations[] = expect('App\Http\Requests')
->classes()
->toExtend('Illuminate\Foundation\Http\FormRequest'); ->toExtend('Illuminate\Foundation\Http\FormRequest');
$this->expectations[] = expect('App\Http\Requests') $this->expectations[] = expect('App\Http\Requests')
@@ -114,6 +116,7 @@ final class Laravel extends AbstractPreset
->toHaveMethod('handle'); ->toHaveMethod('handle');
$this->expectations[] = expect('App\Notifications') $this->expectations[] = expect('App\Notifications')
->classes()
->toExtend('Illuminate\Notifications\Notification'); ->toExtend('Illuminate\Notifications\Notification');
$this->expectations[] = expect('App') $this->expectations[] = expect('App')
@@ -124,6 +127,7 @@ final class Laravel extends AbstractPreset
->toHaveSuffix('ServiceProvider'); ->toHaveSuffix('ServiceProvider');
$this->expectations[] = expect('App\Providers') $this->expectations[] = expect('App\Providers')
->classes()
->toExtend('Illuminate\Support\ServiceProvider'); ->toExtend('Illuminate\Support\ServiceProvider');
$this->expectations[] = expect('App\Providers') $this->expectations[] = expect('App\Providers')
@@ -146,10 +150,10 @@ final class Laravel extends AbstractPreset
->toHaveSuffix('Controller'); ->toHaveSuffix('Controller');
$this->expectations[] = expect('App\Http') $this->expectations[] = expect('App\Http')
->toOnlyBeUsedIn('App\Http'); ->toOnlyBeUsedIn(['App\Http', 'App\Providers']);
$this->expectations[] = expect('App\Http\Controllers') $this->expectations[] = expect('App\Http\Controllers')
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy']); ->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']);
$this->expectations[] = expect([ $this->expectations[] = expect([
'dd', 'dd',
@@ -159,5 +163,15 @@ final class Laravel extends AbstractPreset
'exit', 'exit',
'ray', 'ray',
])->not->toBeUsed(); ])->not->toBeUsed();
$this->expectations[] = expect('App\Policies')
->classes()
->toHaveSuffix('Policy');
$this->expectations[] = expect('App\Attributes')
->classes()
->toImplement('Illuminate\Contracts\Container\ContextualAttribute')
->toHaveAttribute('Attribute')
->toHaveMethod('resolve');
} }
} }
-3
View File
@@ -9,9 +9,6 @@ namespace Pest\ArchPresets;
*/ */
final class Php extends AbstractPreset final class Php extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations[] = expect([ $this->expectations[] = expect([
-3
View File
@@ -12,9 +12,6 @@ use Pest\Expectation;
*/ */
final class Relaxed extends AbstractPreset final class Relaxed extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->eachUserNamespace( $this->eachUserNamespace(
-4
View File
@@ -9,9 +9,6 @@ namespace Pest\ArchPresets;
*/ */
final class Security extends AbstractPreset final class Security extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->expectations[] = expect([ $this->expectations[] = expect([
@@ -32,7 +29,6 @@ final class Security extends AbstractPreset
'create_function', 'create_function',
'unserialize', 'unserialize',
'extract', 'extract',
'parse_str',
'mb_parse_str', 'mb_parse_str',
'dl', 'dl',
'assert', 'assert',
-3
View File
@@ -12,9 +12,6 @@ use Pest\Expectation;
*/ */
final class Strict extends AbstractPreset final class Strict extends AbstractPreset
{ {
/**
* Executes the arch preset.
*/
public function execute(): void public function execute(): void
{ {
$this->eachUserNamespace( $this->eachUserNamespace(
+1 -6
View File
@@ -13,11 +13,9 @@ use PHPUnit\Util\ExcludeList;
final class BootExcludeList implements Bootstrapper final class BootExcludeList implements Bootstrapper
{ {
/** /**
* The directories to exclude.
*
* @var array<int, non-empty-string> * @var array<int, non-empty-string>
*/ */
private const EXCLUDE_LIST = [ private const array EXCLUDE_LIST = [
'bin', 'bin',
'overrides', 'overrides',
'resources', 'resources',
@@ -25,9 +23,6 @@ final class BootExcludeList implements Bootstrapper
'stubs', 'stubs',
]; ];
/**
* Boots the "exclude list" for PHPUnit to ignore Pest files.
*/
public function boot(): void public function boot(): void
{ {
$baseDirectory = dirname(__DIR__, 2); $baseDirectory = dirname(__DIR__, 2);
+7 -10
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\Bootstrappers; namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper; use Pest\Contracts\Bootstrapper;
use Pest\Exceptions\FatalException;
use Pest\Support\DatasetInfo; use Pest\Support\DatasetInfo;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
@@ -20,11 +21,9 @@ use function Pest\testDirectory;
final class BootFiles implements Bootstrapper final class BootFiles implements Bootstrapper
{ {
/** /**
* The structure of the tests directory.
*
* @var array<int, string> * @var array<int, string>
*/ */
private const STRUCTURE = [ private const array STRUCTURE = [
'Expectations', 'Expectations',
'Expectations.php', 'Expectations.php',
'Helpers', 'Helpers',
@@ -32,14 +31,15 @@ final class BootFiles implements Bootstrapper
'Pest.php', 'Pest.php',
]; ];
/**
* Boots the structure of the tests directory.
*/
public function boot(): void public function boot(): void
{ {
$rootPath = TestSuite::getInstance()->rootPath; $rootPath = TestSuite::getInstance()->rootPath;
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory(); $testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();
if (! is_dir($testsPath)) {
throw new FatalException(sprintf('The test directory [%s] does not exist.', $testsPath));
}
foreach (self::STRUCTURE as $filename) { foreach (self::STRUCTURE as $filename) {
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename); $filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
@@ -62,9 +62,6 @@ final class BootFiles implements Bootstrapper
$this->bootDatasets($testsPath); $this->bootDatasets($testsPath);
} }
/**
* Loads, if possible, the given file.
*/
private function load(string $filename): void private function load(string $filename): void
{ {
if (! Str::endsWith($filename, '.php')) { if (! Str::endsWith($filename, '.php')) {
@@ -78,7 +75,7 @@ final class BootFiles implements Bootstrapper
private function bootDatasets(string $testsPath): void private function bootDatasets(string $testsPath): void
{ {
assert(strlen($testsPath) > 0); assert($testsPath !== '');
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
+1 -7
View File
@@ -14,18 +14,12 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final readonly class BootKernelDump implements Bootstrapper final readonly class BootKernelDump implements Bootstrapper
{ {
/**
* Creates a new Boot Kernel Dump instance.
*/
public function __construct( public function __construct(
private OutputInterface $output, private OutputInterface $output,
) { ) {
// ... //
} }
/**
* Boots the kernel dump.
*/
public function boot(): void public function boot(): void
{ {
Container::getInstance()->add(KernelDump::class, $kernelDump = new KernelDump( Container::getInstance()->add(KernelDump::class, $kernelDump = new KernelDump(
+12 -15
View File
@@ -13,24 +13,21 @@ use Pest\Exceptions\ShouldNotHappen;
final class BootOverrides implements Bootstrapper final class BootOverrides implements Bootstrapper
{ {
/** /**
* The list of files to be overridden. * @var array<int, string>
*
* @var array<string, string>
*/ */
public const FILES = [ public const array FILES = [
'c96b1cb57d7fc8e649f4c13a8abe418c2541bcfab194fb6702b99f777f52ee84' => 'Runner/Filter/NameFilterIterator.php', 'ParaTest/WrapperRunner/ProgressPrinterOutput.php',
'a4a43de01f641c6944ee83d963795a46d32b5206b5ab3bbc6cce76e67190acbf' => 'Runner/ResultCache/DefaultResultCache.php', 'Runner/Filter/NameFilterIterator.php',
'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php', 'Runner/TestRunHistory/DefaultTestRunHistory.php',
'3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', 'Runner/TestSuiteLoader.php',
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', 'Runner/TestSuiteSorter.php',
'43883b7e5811886cf3731c8ed6304d5a77078d9731e1e505abc2da36bde19f3e' => 'TextUI/TestSuiteFilterProcessor.php', 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php',
'357d5cd7007f8559b26e1b8cdf43bb6fb15b51b79db981779da6f31b7ec39dad' => 'Event/Value/ThrowableBuilder.php', 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'676273f1fe483877cf2d95c5aedbf9ae5d6a8e2f4c12d6ce716df6591e6db023' => 'Logging/JUnit/JunitXmlLogger.php', 'TextUI/TestSuiteFilterProcessor.php',
'Event/Value/ThrowableBuilder.php',
'Logging/JUnit/JunitXmlLogger.php',
]; ];
/**
* Boots the list of files to be overridden.
*/
public function boot(): void public function boot(): void
{ {
foreach (self::FILES as $file) { foreach (self::FILES as $file) {
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
use PHPUnit\TextUI\Configuration\Builder;
/**
* @internal
*/
final class BootPhpUnitConfiguration implements Bootstrapper
{
public function boot(): void
{
(new Builder)->build(['pest']);
}
}
+18 -9
View File
@@ -16,27 +16,36 @@ use PHPUnit\Event\Subscriber;
final readonly class BootSubscribers implements Bootstrapper final readonly class BootSubscribers implements Bootstrapper
{ {
/** /**
* The list of Subscribers.
*
* @var array<int, class-string<Subscriber>> * @var array<int, class-string<Subscriber>>
*/ */
private const SUBSCRIBERS = [ private const array SUBSCRIBERS = [
Subscribers\EnsureConfigurationIsAvailable::class, Subscribers\EnsureConfigurationIsAvailable::class,
Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
Subscribers\EnsureKernelDumpIsFlushed::class, Subscribers\EnsureKernelDumpIsFlushed::class,
Subscribers\EnsureTeamCityEnabled::class, Subscribers\EnsureTeamCityEnabled::class,
Subscribers\EnsureTiaIsRunningPestTestsOnly::class,
Subscribers\EnsureTiaStarts::class,
Subscribers\EnsureTiaEnds::class,
Subscribers\EnsureTiaResultsAreCollected::class,
Subscribers\EnsureTiaResultIsRecordedOnPassed::class,
Subscribers\EnsureTiaResultIsRecordedOnFailed::class,
Subscribers\EnsureTiaResultIsRecordedOnErrored::class,
Subscribers\EnsureTiaResultIsRecordedOnSkipped::class,
Subscribers\EnsureTiaResultIsRecordedOnIncomplete::class,
Subscribers\EnsureTiaResultIsRecordedOnRisky::class,
Subscribers\EnsureTiaResultIsRecordedOnNoticeTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnPhpNoticeTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnDeprecationTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnPhpDeprecationTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnWarningTriggered::class,
Subscribers\EnsureTiaResultIsRecordedOnPhpWarningTriggered::class,
Subscribers\EnsureTiaAssertionsAreRecordedOnFinished::class,
]; ];
/**
* Creates a new instance of the Boot Subscribers.
*/
public function __construct( public function __construct(
private Container $container, private Container $container,
) {} ) {}
/**
* Boots the list of Subscribers.
*/
public function boot(): void public function boot(): void
{ {
foreach (self::SUBSCRIBERS as $subscriber) { foreach (self::SUBSCRIBERS as $subscriber) {
+1 -7
View File
@@ -13,18 +13,12 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final readonly class BootView implements Bootstrapper final readonly class BootView implements Bootstrapper
{ {
/**
* Creates a new instance of the Boot View.
*/
public function __construct( public function __construct(
private OutputInterface $output private OutputInterface $output
) { ) {
// .. //
} }
/**
* Boots the view renderer.
*/
public function boot(): void public function boot(): void
{ {
View::renderUsing($this->output); View::renderUsing($this->output);
-12
View File
@@ -16,22 +16,13 @@ use function Termwind\renderUsing;
*/ */
final class Events final class Events
{ {
/**
* Sets the output.
*/
private static ?OutputInterface $output = null; private static ?OutputInterface $output = null;
/**
* Sets the output.
*/
public static function setOutput(OutputInterface $output): void public static function setOutput(OutputInterface $output): void
{ {
self::$output = $output; self::$output = $output;
} }
/**
* Fires before the test method description is printed.
*/
public static function beforeTestMethodDescription(TestResult $result, string $description): string public static function beforeTestMethodDescription(TestResult $result, string $description): string
{ {
if (($context = $result->context) === []) { if (($context = $result->context) === []) {
@@ -73,9 +64,6 @@ final class Events
return $description; return $description;
} }
/**
* Fires after the test method description is printed.
*/
public static function afterTestMethodDescription(TestResult $result): void public static function afterTestMethodDescription(TestResult $result): void
{ {
if (($context = $result->context) === []) { if (($context = $result->context) === []) {
-2
View File
@@ -14,8 +14,6 @@ trait Expectable
/** /**
* @template TValue * @template TValue
* *
* Creates a new Expectation.
*
* @param TValue $value * @param TValue $value
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
+3 -6
View File
@@ -8,27 +8,24 @@ use Closure;
/** /**
* @internal * @internal
*
* @template T of object
*/ */
trait Extendable trait Extendable
{ {
/** /**
* The list of extends.
*
* @var array<string, Closure> * @var array<string, Closure>
*/ */
private static array $extends = []; private static array $extends = [];
/** /**
* Register a new extend. * @param-closure-this T $extend
*/ */
public function extend(string $name, Closure $extend): void public function extend(string $name, Closure $extend): void
{ {
static::$extends[$name] = $extend; static::$extends[$name] = $extend;
} }
/**
* Checks if given extend name is registered.
*/
public static function hasExtend(string $name): bool public static function hasExtend(string $name): bool
{ {
return array_key_exists($name, static::$extends); return array_key_exists($name, static::$extends);
-12
View File
@@ -9,33 +9,21 @@ namespace Pest\Concerns\Logging;
*/ */
trait WritesToConsole trait WritesToConsole
{ {
/**
* Writes the given success message to the console.
*/
private function writeSuccess(string $message): void private function writeSuccess(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-green, bold', '✓'); $this->writePestTestOutput($message, 'fg-green, bold', '✓');
} }
/**
* Writes the given error message to the console.
*/
private function writeError(string $message): void private function writeError(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-red, bold', ''); $this->writePestTestOutput($message, 'fg-red, bold', '');
} }
/**
* Writes the given warning message to the console.
*/
private function writeWarning(string $message): void private function writeWarning(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-yellow, bold', '-'); $this->writePestTestOutput($message, 'fg-yellow, bold', '-');
} }
/**
* Writes the give message to the console.
*/
private function writePestTestOutput(string $message, string $color, string $symbol): void private function writePestTestOutput(string $message, string $color, string $symbol): void
{ {
$this->writeWithColor($color, "$symbol ", false); $this->writeWithColor($color, "$symbol ", false);
+1 -12
View File
@@ -12,30 +12,21 @@ use Closure;
trait Pipeable trait Pipeable
{ {
/** /**
* The list of pipes.
*
* @var array<string, array<Closure(Closure, mixed ...$arguments): void>> * @var array<string, array<Closure(Closure, mixed ...$arguments): void>>
*/ */
private static array $pipes = []; private static array $pipes = [];
/** /**
* The list of interceptors.
*
* @var array<string, array<Closure(Closure, mixed ...$arguments): void>> * @var array<string, array<Closure(Closure, mixed ...$arguments): void>>
*/ */
private static array $interceptors = []; private static array $interceptors = [];
/**
* Register a pipe to be applied before an expectation is checked.
*/
public function pipe(string $name, Closure $pipe): void public function pipe(string $name, Closure $pipe): void
{ {
self::$pipes[$name][] = $pipe; self::$pipes[$name][] = $pipe;
} }
/** /**
* Register an interceptor that should replace an existing expectation.
*
* @param string|Closure(mixed $value, mixed ...$arguments):bool $filter * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter
*/ */
public function intercept(string $name, string|Closure $filter, Closure $handler): void public function intercept(string $name, string|Closure $filter, Closure $handler): void
@@ -60,12 +51,10 @@ trait Pipeable
} }
/** /**
* Get the list of pipes by the given name.
*
* @return array<int, Closure> * @return array<int, Closure>
*/ */
private function pipes(string $name, object $context, string $scope): array private function pipes(string $name, object $context, string $scope): array
{ {
return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); return array_map(fn (Closure $pipe): Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
} }
} }
-2
View File
@@ -11,8 +11,6 @@ trait Retrievable
{ {
/** /**
* @template TRetrievableValue * @template TRetrievableValue
*
* Safely retrieve the value at the given key from an object or array.
* @template TRetrievableValue * @template TRetrievableValue
* *
* @param array<string, TRetrievableValue>|object $value * @param array<string, TRetrievableValue>|object $value
+200 -138
View File
@@ -6,13 +6,25 @@ namespace Pest\Concerns;
use Closure; use Closure;
use Pest\Exceptions\DatasetArgumentsMismatch; use Pest\Exceptions\DatasetArgumentsMismatch;
use Pest\Panic;
use Pest\Plugins\Tia;
use Pest\Plugins\Tia\Collectors;
use Pest\Plugins\Tia\Enums\ReplayType;
use Pest\Plugins\Tia\Recorder;
use Pest\Preset; use Pest\Preset;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\Container;
use Pest\Support\ExceptionTrace; use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection; use Pest\Support\Reflection;
use Pest\Support\Shell;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Attributes\PostCondition; use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\IncompleteTest;
use PHPUnit\Framework\SkippedTest;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestCase\ExceptionExpectation;
use PHPUnit\Framework\TestCase\OutputBuffer;
use ReflectionException; use ReflectionException;
use ReflectionFunction; use ReflectionFunction;
use ReflectionParameter; use ReflectionParameter;
@@ -25,113 +37,53 @@ use Throwable;
*/ */
trait Testable trait Testable
{ {
/**
* The test's description.
*/
private string $__description; private string $__description;
/**
* The test's latest description.
*/
private static string $__latestDescription; private static string $__latestDescription;
/**
* The test's assignees.
*/
private static array $__latestAssignees = []; private static array $__latestAssignees = [];
/**
* The test's notes.
*/
private static array $__latestNotes = []; private static array $__latestNotes = [];
/** /**
* The test's issues.
*
* @var array<int, int> * @var array<int, int>
*/ */
private static array $__latestIssues = []; private static array $__latestIssues = [];
/** /**
* The test's PRs.
*
* @var array<int, int> * @var array<int, int>
*/ */
private static array $__latestPrs = []; private static array $__latestPrs = [];
/** /**
* The test's describing, if any. * @var array<int, string>
*/ */
public ?string $__describing = null; public array $__describing = [];
/**
* Whether the test has ran or not.
*/
public bool $__ran = false; public bool $__ran = false;
/** private ReplayType $__replay = ReplayType::None;
* The test's test closure.
*/ private int $__replayAssertions = 0;
private Closure $__test; private Closure $__test;
/**
* The test's before each closure.
*/
private ?Closure $__beforeEach = null; private ?Closure $__beforeEach = null;
/**
* The test's after each closure.
*/
private ?Closure $__afterEach = null; private ?Closure $__afterEach = null;
/**
* The test's before all closure.
*/
private static ?Closure $__beforeAll = null; private static ?Closure $__beforeAll = null;
/**
* The test's after all closure.
*/
private static ?Closure $__afterAll = null; private static ?Closure $__afterAll = null;
/**
* The list of snapshot changes, if any.
*/
private array $__snapshotChanges = []; private array $__snapshotChanges = [];
/**
* Creates a new Test Case instance.
*/
public function __construct(string $name)
{
parent::__construct($name);
$test = TestSuite::getInstance()->tests->get(self::$__filename);
if ($test->hasMethod($name)) {
$method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing;
$this->__test = $method->getClosure();
}
}
/**
* Resets the test case static properties.
*/
public static function flush(): void public static function flush(): void
{ {
self::$__beforeAll = null; self::$__beforeAll = null;
self::$__afterAll = null; self::$__afterAll = null;
} }
/**
* Adds a new "note" to the Test Case.
*/
public function note(array|string $note): self public function note(array|string $note): self
{ {
$note = is_array($note) ? $note : [$note]; $note = is_array($note) ? $note : [$note];
@@ -141,12 +93,9 @@ trait Testable
return $this; return $this;
} }
/**
* Adds a new "setUpBeforeClass" to the Test Case.
*/
public function __addBeforeAll(?Closure $hook): void public function __addBeforeAll(?Closure $hook): void
{ {
if (! $hook instanceof \Closure) { if (! $hook instanceof Closure) {
return; return;
} }
@@ -155,12 +104,9 @@ trait Testable
: $hook; : $hook;
} }
/**
* Adds a new "tearDownAfterClass" to the Test Case.
*/
public function __addAfterAll(?Closure $hook): void public function __addAfterAll(?Closure $hook): void
{ {
if (! $hook instanceof \Closure) { if (! $hook instanceof Closure) {
return; return;
} }
@@ -169,28 +115,19 @@ trait Testable
: $hook; : $hook;
} }
/**
* Adds a new "setUp" to the Test Case.
*/
public function __addBeforeEach(?Closure $hook): void public function __addBeforeEach(?Closure $hook): void
{ {
$this->__addHook('__beforeEach', $hook); $this->__addHook('__beforeEach', $hook);
} }
/**
* Adds a new "tearDown" to the Test Case.
*/
public function __addAfterEach(?Closure $hook): void public function __addAfterEach(?Closure $hook): void
{ {
$this->__addHook('__afterEach', $hook); $this->__addHook('__afterEach', $hook);
} }
/**
* Adds a new "hook" to the Test Case.
*/
private function __addHook(string $property, ?Closure $hook): void private function __addHook(string $property, ?Closure $hook): void
{ {
if (! $hook instanceof \Closure) { if (! $hook instanceof Closure) {
return; return;
} }
@@ -199,9 +136,6 @@ trait Testable
: $hook; : $hook;
} }
/**
* This method is called before the first test of this Test Case is run.
*/
public static function setUpBeforeClass(): void public static function setUpBeforeClass(): void
{ {
parent::setUpBeforeClass(); parent::setUpBeforeClass();
@@ -212,12 +146,13 @@ trait Testable
$beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll); $beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll);
} }
call_user_func(Closure::bind($beforeAll, null, self::class)); try {
call_user_func(Closure::bind($beforeAll, null, self::class));
} catch (Throwable $e) {
Panic::with($e);
}
} }
/**
* This method is called after the last test of this Test Case is run.
*/
public static function tearDownAfterClass(): void public static function tearDownAfterClass(): void
{ {
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); $afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
@@ -231,17 +166,12 @@ trait Testable
parent::tearDownAfterClass(); parent::tearDownAfterClass();
} }
/**
* Gets executed before the Test Case.
*/
protected function setUp(...$arguments): void protected function setUp(...$arguments): void
{ {
TestSuite::getInstance()->test = $this; TestSuite::getInstance()->test = $this;
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$method->setUp($this);
$description = $method->description; $description = $method->description;
if ($this->dataName()) { if ($this->dataName()) {
$description = str_contains((string) $description, ':dataset') $description = str_contains((string) $description, ':dataset')
@@ -272,8 +202,37 @@ trait Testable
self::$__latestIssues = $method->issues; self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs; self::$__latestPrs = $method->prs;
/** @var Tia $tia */
$tia = Container::getInstance()->get(Tia::class);
$status = $tia->getStatus(self::$__filename, $this->valueObjectForEvents()->id());
$replay = ReplayType::fromStatus($status);
if ($replay !== ReplayType::None) {
assert($status !== null);
$this->__replay = $replay;
match ($replay) {
ReplayType::Pass, ReplayType::Risky => $this->__beginReplay($replay, $tia),
ReplayType::Skipped => $this->markTestSkipped($status->message()),
ReplayType::Incomplete => $this->markTestIncomplete($status->message()),
ReplayType::Failure => throw new AssertionFailedError($status->message() ?: 'Cached failure'),
};
return;
}
$recorder = Container::getInstance()->get(Recorder::class);
assert($recorder instanceof Recorder);
if ($recorder->isActive()) {
$recorder->beginTest($this::class, $this->name(), self::$__filename);
}
parent::setUp(); parent::setUp();
Collectors::armAll($recorder);
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1]; $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1];
if ($this->__beforeEach instanceof Closure) { if ($this->__beforeEach instanceof Closure) {
@@ -283,11 +242,44 @@ trait Testable
$this->__callClosure($beforeEach, $arguments); $this->__callClosure($beforeEach, $arguments);
} }
/** private function __beginReplay(ReplayType $replay, Tia $tia): void
* Gets executed after the Test Case. {
*/ $this->__replay = $replay;
$this->__replayAssertions = $tia->getAssertionCount($this->valueObjectForEvents()->id());
$this->__ran = true;
}
public function __initializeTestCase(): void
{
if (isset($this->__test)) {
return;
}
$name = $this->name();
$test = TestSuite::getInstance()->tests->get(self::$__filename);
if ($test->hasMethod($name)) {
$method = $test->getMethod($name);
$this->__description = self::$__latestDescription = $method->description;
self::$__latestAssignees = $method->assignees;
self::$__latestNotes = $method->notes;
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing;
$this->__test = $method->getClosure();
$method->setUp($this);
}
}
protected function tearDown(...$arguments): void protected function tearDown(...$arguments): void
{ {
if ($this->__replay !== ReplayType::None) {
TestSuite::getInstance()->test = null;
return;
}
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
if ($this->__afterEach instanceof Closure) { if ($this->__afterEach instanceof Closure) {
@@ -307,21 +299,110 @@ trait Testable
} }
/** /**
* Executes the Test Case current test.
*
* @throws Throwable * @throws Throwable
*/ */
private function __runTest(Closure $closure, ...$args): mixed private function __runTest(Closure $closure, ...$args): mixed
{ {
if ($this->__replay === ReplayType::Pass || $this->__replay === ReplayType::Risky) {
if ($this->__replay === ReplayType::Pass && $this->__replayAssertions === 0) {
$this->expectNotToPerformAssertions();
}
$this->addToAssertionCount($this->__replayAssertions);
return null;
}
$arguments = $this->__resolveTestArguments($args); $arguments = $this->__resolveTestArguments($args);
$this->__ensureDatasetArgumentNameAndNumberMatches($arguments); $this->__ensureDatasetArgumentNameAndNumberMatches($arguments);
return $this->__callClosure($closure, $arguments); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
if ($method->flakyTries === null) {
return $this->__callClosure($closure, $arguments);
}
$lastException = null;
$initialProperties = get_object_vars($this);
for ($attempt = 1; $attempt <= $method->flakyTries; $attempt++) {
try {
return $this->__callClosure($closure, $arguments);
} catch (Throwable $e) {
if ($e instanceof SkippedTest
|| $e instanceof IncompleteTest
|| $this->__isExpectedException($e)) {
throw $e;
}
$lastException = $e;
if ($attempt < $method->flakyTries) {
if ($this->__snapshotChanges !== []) {
throw $e;
}
$this->tearDown();
Closure::bind(fn (): array => $this->mockObjects = [], $this, TestCase::class)();
foreach (array_keys(array_diff_key(get_object_vars($this), $initialProperties)) as $property) {
unset($this->{$property});
}
$outputBuffer = Closure::bind(fn () => $this->outputBuffer, $this, TestCase::class)();
if ($outputBuffer->hasExpectation()) {
ob_clean();
Closure::bind(function (): void {
$this->expectedString = null;
$this->expectedRegularExpression = null;
}, $outputBuffer, OutputBuffer::class)();
}
$this->setUp();
}
}
}
throw $lastException;
}
private function __isExpectedException(Throwable $e): bool
{
$expectation = Closure::bind(fn () => $this->exceptionExpectation, $this, TestCase::class)();
$read = fn (string $property): mixed => Closure::bind(fn () => $this->{$property}, $expectation, ExceptionExpectation::class)();
$expectedClass = $read('expectedException');
if ($expectedClass !== null) {
return $e instanceof $expectedClass;
}
$expectedMessage = $read('expectedMessage');
if ($expectedMessage !== null) {
return str_contains($e->getMessage(), (string) $expectedMessage);
}
$expectedMessageRegex = $read('expectedMessageRegularExpression');
if ($expectedMessageRegex !== null) {
return preg_match($expectedMessageRegex, $e->getMessage()) === 1;
}
$expectedCode = $read('expectedCode');
if ($expectedCode !== null) {
return $e->getCode() === $expectedCode;
}
return false;
} }
/** /**
* Resolve the passed arguments. Any Closures will be bound to the testcase and resolved.
*
* @throws Throwable * @throws Throwable
*/ */
private function __resolveTestArguments(array $arguments): array private function __resolveTestArguments(array $arguments): array
@@ -329,16 +410,13 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
if ($method->repetitions > 1) { if ($method->repetitions > 1) {
// If the test is repeated, the first argument is the iteration number
// we need to move it to the end of the arguments list
// so that the datasets are the first n arguments
// and the iteration number is the last argument
$firstArgument = array_shift($arguments); $firstArgument = array_shift($arguments);
$arguments[] = $firstArgument; $arguments[] = $firstArgument;
} }
$underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure');
$testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); $testParameterTypesByName = Reflection::getFunctionArguments($underlyingTest);
$testParameterTypes = array_values($testParameterTypesByName);
if (count($arguments) !== 1) { if (count($arguments) !== 1) {
foreach ($arguments as $argumentIndex => $argumentValue) { foreach ($arguments as $argumentIndex => $argumentValue) {
@@ -346,7 +424,11 @@ trait Testable
continue; continue;
} }
if (in_array($testParameterTypes[$argumentIndex], [Closure::class, 'callable', 'mixed'])) { $parameterType = is_string($argumentIndex)
? $testParameterTypesByName[$argumentIndex]
: $testParameterTypes[$argumentIndex];
if (in_array($parameterType, [Closure::class, 'callable', 'mixed'])) {
continue; continue;
} }
@@ -372,12 +454,10 @@ trait Testable
return [$boundDatasetResult]; return [$boundDatasetResult];
} }
return array_values($boundDatasetResult); return $boundDatasetResult;
} }
/** /**
* Ensures dataset items count matches underlying test case required parameters
*
* @throws ReflectionException * @throws ReflectionException
* @throws DatasetArgumentsMismatch * @throws DatasetArgumentsMismatch
*/ */
@@ -417,9 +497,6 @@ trait Testable
return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
} }
/**
* Uses the given preset on the test.
*/
public function preset(): Preset public function preset(): Preset
{ {
return new Preset; return new Preset;
@@ -432,44 +509,24 @@ trait Testable
return; return;
} }
if (count($this->__snapshotChanges) === 1) { $this->markTestIncomplete(implode('. ', $this->__snapshotChanges));
$this->markTestIncomplete($this->__snapshotChanges[0]);
return;
}
$messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges));
$this->markTestIncomplete($messages);
} }
/**
* The printable test case name.
*/
public static function getPrintableTestCaseName(): string public static function getPrintableTestCaseName(): string
{ {
return preg_replace('/P\\\/', '', self::class, 1); return preg_replace('/P\\\/', '', self::class, 1);
} }
/**
* The printable test case method name.
*/
public function getPrintableTestCaseMethodName(): string public function getPrintableTestCaseMethodName(): string
{ {
return $this->__description; return $this->__description;
} }
/**
* The latest printable test case method name.
*/
public static function getLatestPrintableTestCaseMethodName(): string public static function getLatestPrintableTestCaseMethodName(): string
{ {
return self::$__latestDescription; return self::$__latestDescription ?? '';
} }
/**
* The printable test case method context.
*/
public static function getPrintableContext(): array public static function getPrintableContext(): array
{ {
return [ return [
@@ -479,4 +536,9 @@ trait Testable
'notes' => self::$__latestNotes, 'notes' => self::$__latestNotes,
]; ];
} }
public function shell(): void
{
Shell::open();
}
} }
+23 -37
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\UsesCall; use Pest\PendingCalls\UsesCall;
/** /**
@@ -13,31 +14,19 @@ use Pest\PendingCalls\UsesCall;
*/ */
final readonly class Configuration final readonly class Configuration
{ {
/**
* The filename of the configuration.
*/
private string $filename; private string $filename;
/**
* Creates a new configuration instance.
*/
public function __construct( public function __construct(
string $filename, string $filename,
) { ) {
$this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename; $this->filename = str_ends_with($filename, DIRECTORY_SEPARATOR.'Pest.php') ? dirname($filename) : $filename;
} }
/**
* Use the given classes and traits in the given targets.
*/
public function in(string ...$targets): UsesCall public function in(string ...$targets): UsesCall
{ {
return (new UsesCall($this->filename, []))->in(...$targets); return new UsesCall($this->filename, [])->in(...$targets);
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extend(string ...$classAndTraits): UsesCall public function extend(string ...$classAndTraits): UsesCall
{ {
return new UsesCall( return new UsesCall(
@@ -46,65 +35,62 @@ final readonly class Configuration
); );
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function extends(string ...$classAndTraits): UsesCall public function extends(string ...$classAndTraits): UsesCall
{ {
return $this->extend(...$classAndTraits); return $this->extend(...$classAndTraits);
} }
/**
* Depending on where is called, it will add the given groups globally or locally.
*/
public function group(string ...$groups): UsesCall public function group(string ...$groups): UsesCall
{ {
return (new UsesCall($this->filename, []))->group(...$groups); return new UsesCall($this->filename, [])->group(...$groups);
}
public function only(): void
{
new BeforeEachCall(TestSuite::getInstance(), $this->filename)->only();
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function use(string ...$classAndTraits): UsesCall public function use(string ...$classAndTraits): UsesCall
{ {
return $this->extend(...$classAndTraits); return $this->extend(...$classAndTraits);
} }
/**
* Depending on where is called, it will extend the given classes and traits globally or locally.
*/
public function uses(string ...$classAndTraits): UsesCall public function uses(string ...$classAndTraits): UsesCall
{ {
return $this->extends(...$classAndTraits); return $this->extends(...$classAndTraits);
} }
/**
* Gets the printer configuration.
*/
public function printer(): Configuration\Printer public function printer(): Configuration\Printer
{ {
return new Configuration\Printer; return new Configuration\Printer;
} }
/**
* Gets the presets configuration.
*/
public function presets(): Configuration\Presets public function presets(): Configuration\Presets
{ {
return new Configuration\Presets; return new Configuration\Presets;
} }
/**
* Gets the project configuration.
*/
public function project(): Configuration\Project public function project(): Configuration\Project
{ {
return Configuration\Project::getInstance(); return Configuration\Project::getInstance();
} }
public function browser(): Browser\Configuration
{
return new Browser\Configuration;
}
public function evals(): Evals\Configuration // @phpstan-ignore-line
{
return new Evals\Configuration; // @phpstan-ignore-line
}
public function tia(): Plugins\Tia\Configuration
{
return new Plugins\Tia\Configuration;
}
/** /**
* Proxies calls to the uses method.
*
* @param array<array-key, mixed> $arguments * @param array<array-key, mixed> $arguments
*/ */
public function __call(string $name, array $arguments): mixed public function __call(string $name, array $arguments): mixed
-3
View File
@@ -9,9 +9,6 @@ use Pest\Preset;
final class Presets final class Presets
{ {
/**
* Creates a custom preset instance, and adds it to the list of presets.
*/
public function custom(string $name, Closure $execute): void public function custom(string $name, Closure $execute): void
{ {
Preset::custom($name, $execute); Preset::custom($name, $execute);
-3
View File
@@ -11,9 +11,6 @@ use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter;
*/ */
final readonly class Printer final readonly class Printer
{ {
/**
* Sets the theme to compact.
*/
public function compact(): self public function compact(): self
{ {
DefaultPrinter::compact(true); DefaultPrinter::compact(true);
+6 -31
View File
@@ -10,42 +10,27 @@ namespace Pest\Configuration;
final class Project final class Project
{ {
/** /**
* The assignees link.
*
* @internal * @internal
*/ */
public string $assignees = ''; public string $assignees = '';
/** /**
* The issues link.
*
* @internal * @internal
*/ */
public string $issues = ''; public string $issues = '';
/** /**
* The PRs link.
*
* @internal * @internal
*/ */
public string $prs = ''; public string $prs = '';
/**
* The singleton instance.
*/
private static ?self $instance = null; private static ?self $instance = null;
/**
* Creates a new instance of the project.
*/
public static function getInstance(): self public static function getInstance(): self
{ {
return self::$instance ??= new self; return self::$instance ??= new self;
} }
/**
* Sets the test project to GitHub.
*/
public function github(string $project): self public function github(string $project): self
{ {
$this->issues = "https://github.com/{$project}/issues/%s"; $this->issues = "https://github.com/{$project}/issues/%s";
@@ -56,22 +41,18 @@ final class Project
return $this; return $this;
} }
/** public function gitlab(string $project, string $hostname = 'gitlab.com'): self
* Sets the test project to GitLab.
*/
public function gitlab(string $project): self
{ {
$this->issues = "https://gitlab.com/{$project}/issues/%s"; $hostname = parse_url($hostname, PHP_URL_HOST) ?? $hostname;
$this->prs = "https://gitlab.com/{$project}/merge_requests/%s";
$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; return $this;
} }
/**
* Sets the test project to Bitbucket.
*/
public function bitbucket(string $project): self public function bitbucket(string $project): self
{ {
$this->issues = "https://bitbucket.org/{$project}/issues/%s"; $this->issues = "https://bitbucket.org/{$project}/issues/%s";
@@ -82,9 +63,6 @@ final class Project
return $this; return $this;
} }
/**
* Sets the test project to Jira.
*/
public function jira(string $namespace, string $project): self public function jira(string $namespace, string $project): self
{ {
$this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s"; $this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s";
@@ -94,9 +72,6 @@ final class Project
return $this; return $this;
} }
/**
* Sets the test project to custom.
*/
public function custom(string $issues, string $prs, string $assignees): self public function custom(string $issues, string $prs, string $assignees): self
{ {
$this->issues = $issues; $this->issues = $issues;
+2 -10
View File
@@ -12,11 +12,9 @@ use Symfony\Component\Console\Output\OutputInterface;
final readonly class Help final readonly class Help
{ {
/** /**
* The Command messages.
*
* @var array<int, string> * @var array<int, string>
*/ */
private const HELP_MESSAGES = [ private const array HELP_MESSAGES = [
'<comment>Pest Options:</comment>', '<comment>Pest Options:</comment>',
' <info>--init</info> Initialise a standard Pest configuration', ' <info>--init</info> Initialise a standard Pest configuration',
' <info>--coverage</info> Enable coverage and output to standard output', ' <info>--coverage</info> Enable coverage and output to standard output',
@@ -24,17 +22,11 @@ final readonly class Help
' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)', ' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)',
]; ];
/**
* Creates a new Console Command instance.
*/
public function __construct(private OutputInterface $output) public function __construct(private OutputInterface $output)
{ {
// .. //
} }
/**
* Executes the Console Command.
*/
public function __invoke(): void public function __invoke(): void
{ {
foreach (self::HELP_MESSAGES as $message) { foreach (self::HELP_MESSAGES as $message) {
+8 -12
View File
@@ -18,30 +18,26 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
final readonly class Thanks final readonly class Thanks
{ {
/** /**
* The support options.
*
* @var array<string, string> * @var array<string, string>
*/ */
private const FUNDING_MESSAGES = [ private const array FUNDING_MESSAGES = [
'Star' => 'https://github.com/pestphp/pest', 'Star' => 'https://github.com/pestphp/pest',
'News' => 'https://twitter.com/pestphp', 'YouTube' => 'https://youtube.com/@nunomaduro',
'Videos' => 'https://youtube.com/@nunomaduro', 'TikTok' => 'https://tiktok.com/@enunomaduro',
'Twitch' => 'https://twitch.tv/nunomaduro',
'LinkedIn' => 'https://linkedin.com/in/nunomaduro',
'Instagram' => 'https://instagram.com/enunomaduro',
'X' => 'https://x.com/enunomaduro',
'Sponsor' => 'https://github.com/sponsors/nunomaduro', 'Sponsor' => 'https://github.com/sponsors/nunomaduro',
]; ];
/**
* Creates a new Console Command instance.
*/
public function __construct( public function __construct(
private InputInterface $input, private InputInterface $input,
private OutputInterface $output private OutputInterface $output
) { ) {
// .. //
} }
/**
* Executes the Console Command.
*/
public function __invoke(): void public function __invoke(): void
{ {
$bootstrapper = new BootView($this->output); $bootstrapper = new BootView($this->output);
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts;
*/ */
interface Bootstrapper interface Bootstrapper
{ {
/**
* Boots the bootstrapper.
*/
public function boot(): void; public function boot(): void;
} }
+1 -1
View File
@@ -11,5 +11,5 @@ use NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName as
*/ */
interface HasPrintableTestCaseName extends BaseHasPrintableTestCaseName interface HasPrintableTestCaseName extends BaseHasPrintableTestCaseName
{ {
// .. //
} }
-6
View File
@@ -11,13 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
interface Panicable interface Panicable
{ {
/**
* Renders the panic on the given output.
*/
public function render(OutputInterface $output): void; public function render(OutputInterface $output): void;
/**
* The exit code to be used.
*/
public function exitCode(): int; public function exitCode(): int;
} }
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts\Plugins;
*/ */
interface AddsOutput interface AddsOutput
{ {
/**
* Adds output after the Test Suite execution.
*/
public function addOutput(int $exitCode): int; public function addOutput(int $exitCode): int;
} }
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts\Plugins;
*/ */
interface Bootable interface Bootable
{ {
/**
* Boots the plugin.
*/
public function boot(): void; public function boot(): void;
} }
@@ -10,8 +10,6 @@ namespace Pest\Contracts\Plugins;
interface HandlesArguments interface HandlesArguments
{ {
/** /**
* Adds arguments before the Test Suite execution.
*
* @param array<int, string> $arguments * @param array<int, string> $arguments
* @return array<int, string> * @return array<int, string>
*/ */
@@ -10,8 +10,6 @@ namespace Pest\Contracts\Plugins;
interface HandlesOriginalArguments interface HandlesOriginalArguments
{ {
/** /**
* Adds original arguments before the Test Suite execution.
*
* @param array<int, string> $arguments * @param array<int, string> $arguments
*/ */
public function handleOriginalArguments(array $arguments): void; public function handleOriginalArguments(array $arguments): void;
-3
View File
@@ -9,8 +9,5 @@ namespace Pest\Contracts\Plugins;
*/ */
interface Terminable interface Terminable
{ {
/**
* Terminates the plugin.
*/
public function terminate(): void; public function terminate(): void;
} }
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Pest\Contracts;
/**
* @internal
*/
interface Restarter
{
/**
* @param array<int, string> $arguments
*/
public function maybeRestart(string $projectRoot, array $arguments): void;
}
-3
View File
@@ -6,8 +6,5 @@ namespace Pest\Contracts;
interface TestCaseFilter interface TestCaseFilter
{ {
/**
* Whether the test case is accepted.
*/
public function accept(string $testCaseFilename): bool; public function accept(string $testCaseFilename): bool;
} }
-3
View File
@@ -8,8 +8,5 @@ use Pest\Factories\TestCaseMethodFactory;
interface TestCaseMethodFilter interface TestCaseMethodFilter
{ {
/**
* Whether the test case method is accepted.
*/
public function accept(TestCaseMethodFactory $factory): bool; public function accept(TestCaseMethodFactory $factory): bool;
} }
-2
View File
@@ -12,8 +12,6 @@ use Pest\Factories\Attribute;
final class Attributes final class Attributes
{ {
/** /**
* Evaluates the given attributes and returns the code.
*
* @param iterable<int, Attribute> $attributes * @param iterable<int, Attribute> $attributes
*/ */
public static function code(iterable $attributes): string public static function code(iterable $attributes): string
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The afterAll already exists in the filename `%s`.', $filename)); parent::__construct(sprintf('The [afterAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class AfterAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class AfterAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename `%s`.', $filename)); parent::__construct(sprintf('The [afterAll] hook may not be used inside a [describe] block. Please move it to the top level of [%s].', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class AfterBeforeTestFunction extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class AfterBeforeTestFunction extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct('After method cannot be used with before the [test|it] functions in the filename `['.$filename.']`.'); parent::__construct(sprintf('The [after] hook may only be chained onto [beforeEach] inside a [describe] block. Please move it inside one in [%s].', $filename));
} }
} }
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use Pest\Support\View;
use RuntimeException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class BaselineFetchFailed extends RuntimeException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function __construct(
private readonly string $headline,
private readonly string $hint,
private readonly bool $hasAnchor = false,
) {
parent::__construct($headline);
}
public function render(OutputInterface $output): void
{
View::renderUsing($output);
if (! $this->hasAnchor) {
View::render('components.badge', ['type' => 'ERROR', 'content' => $this->headline]);
$this->renderChild($output, $this->hint.' Or use [--fresh] to record locally.');
$output->writeln('');
return;
}
$this->renderChild($output, $this->headline);
$this->renderChild($output, $this->hint.' Or use [--fresh] to record locally.');
$output->writeln('');
}
public function exitCode(): int
{
return 1;
}
private function renderChild(OutputInterface $output, string $text): void
{
$output->writeln(sprintf(' <fg=gray>─ %s</>', $text));
}
}
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class BeforeAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class BeforeAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The beforeAll already exists in the filename `%s`.', $filename)); parent::__construct(sprintf('The [beforeAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class BeforeAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class BeforeAllWithinDescribe extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename `%s`.', $filename)); parent::__construct(sprintf('The [beforeAll] hook may not be used inside a [describe] block. Please move it to the top level of [%s].', $filename));
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class DatasetAlreadyExists extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class DatasetAlreadyExists extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $name, string $scope) public function __construct(string $name, string $scope)
{ {
parent::__construct(sprintf('A dataset with the name `%s` already exists in scope [%s].', $name, $scope)); parent::__construct(sprintf('A dataset named [%s] is already registered in [%s]. Please choose a different name.', $name, $scope));
} }
} }
+2 -4
View File
@@ -11,11 +11,9 @@ final class DatasetArgumentsMismatch extends Exception
public function __construct(int $requiredCount, int $suppliedCount) public function __construct(int $requiredCount, int $suppliedCount)
{ {
if ($requiredCount <= $suppliedCount) { if ($requiredCount <= $suppliedCount) {
parent::__construct('Test argument names and dataset keys do not match'); parent::__construct('The test arguments do not match the dataset keys. Please make sure each argument is named after a key in the dataset.');
} else { } else {
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount)); parent::__construct(sprintf('The test expects [%d] argument(s), but the dataset only provides [%d].', $requiredCount, $suppliedCount));
} }
} }
//
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $name) public function __construct(string $name)
{ {
parent::__construct(sprintf("A dataset with the name `%s` does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name)); parent::__construct(sprintf("A dataset named [%s] does not exist. You may create one using `dataset('%s', ['a', 'b']);`.", $name, $name));
} }
} }
+2 -4
View File
@@ -15,18 +15,16 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/** /**
* Creates a new Exception instance.
*
* @param array<string, string> $arguments * @param array<string, string> $arguments
*/ */
public function __construct(string $file, string $name, array $arguments) public function __construct(string $file, string $name, array $arguments)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
"A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'The test [%s] in [%s] expects [%d] argument(s) ([%s]), but no dataset was provided. Please chain [with()] onto the test to supply one.',
$name, $name,
$file,
count($arguments), count($arguments),
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)), implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
$file,
)); ));
} }
} }
+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);
}
}
+1 -4
View File
@@ -11,11 +11,8 @@ use Exception;
*/ */
final class ExpectationNotFound extends Exception final class ExpectationNotFound extends Exception
{ {
/**
* Creates a new ExpectationNotFound instance from the given name.
*/
public static function fromName(string $name): ExpectationNotFound public static function fromName(string $name): ExpectationNotFound
{ {
return new self("Expectation [$name] does not exist."); return new self("The expectation [$name] does not exist. You may register it using [expect()->extend()].");
} }
} }
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The file or folder with the name `%s` could not be found.', $filename)); parent::__construct(sprintf('The file or folder [%s] could not be found. Please check the path and try again.', $filename));
} }
} }
@@ -14,9 +14,6 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $message) public function __construct(string $message)
{ {
parent::__construct($message, 1); parent::__construct($message, 1);
+1 -1
View File
@@ -21,6 +21,6 @@ final class InvalidExpectation extends LogicException implements ExceptionInterf
*/ */
public static function fromMethods(array $methods): never public static function fromMethods(array $methods): never
{ {
throw new self(sprintf('Expectation [%s] is not valid.', implode('->', $methods))); throw new self(sprintf('The expectation [%s] does not exist. Please check the spelling, or register it using [expect()->extend()].', implode('->', $methods)));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class InvalidExpectationValue extends InvalidArgumentException
*/ */
public static function expected(string $type): never public static function expected(string $type): never
{ {
throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type)); throw new self(sprintf('This expectation may only be used on a value of type [%s].', $type));
} }
} }
-3
View File
@@ -14,9 +14,6 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class InvalidOption extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class InvalidOption extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $message) public function __construct(string $message)
{ {
parent::__construct($message, 1); parent::__construct($message, 1);
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct() public function __construct()
{ {
parent::__construct('Please run [./vendor/bin/pest] instead.'); parent::__construct('Pest must be run through its own binary. Please run [./vendor/bin/pest] instead.');
} }
} }
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface;
/**
* @internal
*/
final class InvalidTestClassName extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
public static function fromClassName(string $filename, string $className): self
{
return new self(sprintf(
'The test file [%s] would create the class [%s], which is not a valid PHP class name. Please rename the file.',
$filename,
$className,
));
}
public static function fromNamespace(string $filename, string $namespace, string $part): self
{
return new self(sprintf(
'The test file [%s] would create the namespace [%s], which is not a valid PHP namespace, because [%s] may not be used as a namespace name. Please rename that folder.',
$filename,
$namespace,
$part,
));
}
}
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $feature, string $dependency) public function __construct(string $feature, string $dependency)
{ {
parent::__construct(sprintf('The feature "%s" requires "%s".', $feature, $dependency)); parent::__construct(sprintf('The [%s] feature requires [%s]. Please install it and try again.', $feature, $dependency));
} }
} }
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Pest\Exceptions;
use InvalidArgumentException;
use NunoMaduro\Collision\Contracts\RenderlessEditor;
use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Pest\Contracts\Panicable;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class NoAffectedTestsFound extends InvalidArgumentException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{
public function render(OutputInterface $output): void
{
$output->writeln([
'',
' <fg=white;options=bold;bg=blue> INFO </> No affected tests found.',
'',
]);
}
public function exitCode(): int
{
return 0;
}
}
-6
View File
@@ -16,9 +16,6 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, Panicable, RenderlessEditor, RenderlessTrace
{ {
/**
* Renders the panic on the given output.
*/
public function render(OutputInterface $output): void public function render(OutputInterface $output): void
{ {
$output->writeln([ $output->writeln([
@@ -28,9 +25,6 @@ final class NoDirtyTestsFound extends InvalidArgumentException implements Except
]); ]);
} }
/**
* The exit code to be used.
*/
public function exitCode(): int public function exitCode(): int
{ {
return 0; return 0;
+1 -7
View File
@@ -12,15 +12,12 @@ use RuntimeException;
*/ */
final class ShouldNotHappen extends RuntimeException final class ShouldNotHappen extends RuntimeException
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(Exception $exception) public function __construct(Exception $exception)
{ {
$message = $exception->getMessage(); $message = $exception->getMessage();
parent::__construct(sprintf(<<<'EOF' parent::__construct(sprintf(<<<'EOF'
This should not happen - please create an new issue here: https://github.com/pestphp/pest. This should not have happened. Please report it here: https://github.com/pestphp/pest/issues
Issue: %s Issue: %s
PHP version: %s PHP version: %s
@@ -29,9 +26,6 @@ EOF
, $message, phpversion(), PHP_OS), 1, $exception); , $message, phpversion(), PHP_OS), 1, $exception);
} }
/**
* Creates a new instance of should not happen without a specific exception.
*/
public static function fromMessage(string $message): ShouldNotHappen public static function fromMessage(string $message): ShouldNotHappen
{ {
return new ShouldNotHappen(new Exception($message)); return new ShouldNotHappen(new Exception($message));
+1 -4
View File
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $fileName, string $description) public function __construct(string $fileName, string $description)
{ {
parent::__construct(sprintf('A test with the description `%s` already exists in the filename `%s`.', $description, $fileName)); parent::__construct(sprintf('A test named [%s] already exists in [%s]. Please give this test a different description.', $description, $fileName));
} }
} }
+6 -5
View File
@@ -14,12 +14,13 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $inUse, string $newOne, string $folder) public function __construct(string $inUse, string $newOne, string $folder)
{ {
parent::__construct(sprintf('Test case `%s` can not be used. The folder `%s` already uses the test case `%s`', parent::__construct(sprintf(
$newOne, $folder, $inUse)); 'The test case [%s] may not be used here. The folder [%s] is already bound to the test case [%s].',
$newOne,
$folder,
$inUse,
));
} }
} }
@@ -14,11 +14,8 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
*/ */
final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{ {
/**
* Creates a new Exception instance.
*/
public function __construct(string $testCaseClass) public function __construct(string $testCaseClass)
{ {
parent::__construct(sprintf('The class `%s` was not found.', $testCaseClass)); parent::__construct(sprintf('The class or trait [%s] could not be found. Please check the name, and make sure it is autoloadable.', $testCaseClass));
} }
} }

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