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