fix: stacktrace with nested with calls

This commit is contained in:
nuno maduro
2026-04-10 17:25:05 +01:00
parent bdf60cea91
commit 729f18a152
5 changed files with 349 additions and 4 deletions

View File

@ -74,7 +74,7 @@ final class DescribeCall
*/
public function __call(string $name, array $arguments): self
{
if (! $this->currentBeforeEachCall instanceof \Pest\PendingCalls\BeforeEachCall) {
if (! $this->currentBeforeEachCall instanceof BeforeEachCall) {
$this->currentBeforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $this->filename);
$this->currentBeforeEachCall->describing = array_merge(

View File

@ -23,7 +23,9 @@ final class Backtrace
$current = null;
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) {
assert(array_key_exists(self::FILE, $trace));
if (array_key_exists(self::FILE, $trace) === false) {
break;
}
$traceFile = str_replace(DIRECTORY_SEPARATOR, '/', $trace[self::FILE]);

View File

@ -95,6 +95,48 @@
PASS Tests\Features\Covers\TraitCoverage
✓ it uses the correct PHPUnit attribute for trait
PASS Tests\Features\DatasetMethodChaining
✓ beforeEach()->with() applies dataset to tests → receives the dataset value with (10)
✓ beforeEach()->with() applies dataset to tests → it also receives the dataset value in it() with (10)
✓ beforeEach()->with() with multiple dataset values → receives each value from the dataset with (1)
✓ beforeEach()->with() with multiple dataset values → receives each value from the dataset with (2)
✓ beforeEach()->with() with multiple dataset values → receives each value from the dataset with (3)
✓ beforeEach()->with() with keyed dataset → receives keyed dataset values with dataset "first"
✓ beforeEach()->with() with keyed dataset → receives keyed dataset values with dataset "second"
✓ beforeEach()->with() with closure dataset → receives values from closure dataset with (100)
✓ beforeEach()->with() with closure dataset → receives values from closure dataset with (200)
✓ describe()->with() passes dataset to tests → receives the dataset value with (42)
✓ describe()->with() passes dataset to tests → it also receives it in it() with (42)
✓ describe()->with() with multiple values → receives each value with (5)
✓ describe()->with() with multiple values → receives each value with (10)
✓ describe()->with() with multiple values → receives each value with (15)
✓ describe()->with() with keyed dataset → receives keyed values with dataset "alpha"
✓ describe()->with() with keyed dataset → receives keyed values with dataset "beta"
✓ describe()->with() with closure dataset → receives closure dataset values with (7)
✓ describe()->with() with closure dataset → receives closure dataset values with (14)
✓ outer with dataset → inner without dataset → inherits outer dataset with (1)
✓ nested describe blocks with datasets at multiple levels → level 1 → receives level 1 dataset with (10)
✓ nested describe blocks with datasets at multiple levels → level 1 → level 2 → receives datasets from all ancestor levels with (10) / (20)
✓ deeply nested describe with datasets → a → b → c → receives all ancestor datasets with (1) / (2) / (3)
✓ beforeEach()->with() combined with test->with() → receives both datasets as cross product with (10) / (1)
✓ beforeEach()->with() combined with test->with() → receives both datasets as cross product with (10) / (2)
✓ describe()->with() combined with test->with() → receives both datasets with (5) / (50)
✓ describe()->with() combined with test->with() → receives both datasets with (5) / (60)
✓ beforeEach closure and beforeEach()->with() coexist → has both the closure state and dataset with (99)
✓ beforeEach()->with() does not interfere with closure hooks → closures run in order and dataset is applied with (42)
✓ first describe with dataset → gets its own dataset with (111)
✓ second describe with different dataset → gets its own dataset, not the sibling with (222)
✓ third describe without dataset → has no dataset leaking from siblings
✓ describe()->with() with beforeEach closure → both hook and dataset work with (77)
✓ describe()->with() with afterEach closure → dataset is available and afterEach runs with (88)
✓ multiple tests share the same beforeEach dataset → first test gets the dataset with (33)
✓ multiple tests share the same beforeEach dataset → second test also gets the dataset with (33)
✓ multiple tests share the same beforeEach dataset → it third test with it() also gets the dataset with (33)
✓ outer describe → inner describe with dataset on hook → inherits outer beforeEach and has inner dataset with (55)
✓ outer describe → outer test is unaffected by inner dataset
✓ describe()->with() preserves depends → first with (9)
✓ describe()->with() preserves depends → second with (9)
PASS Tests\Features\DatasetsTests - 1 todo
✓ it throws exception if dataset does not exist
✓ it throws exception if dataset already exist
@ -215,6 +257,20 @@
✓ it may be used with high order after describe block with dataset "formal"
✓ it may be used with high order after describe block with dataset "informal"
✓ after describe block with named dataset with ('after')
✓ named parameters match by parameter name with ('Taylor', 'taylor@laravel.com')
✓ named parameters work with multiple dataset items with ('Taylor', 'taylor@laravel.com')
✓ named parameters work with multiple dataset items with ('James', 'james@laravel.com')
✓ named parameters work in different order than closure params with ('a', 'b', 'c')
✓ named parameters work with named dataset keys with dataset "taylor"
✓ named parameters work with named dataset keys with dataset "james"
✓ named parameters work with closures that should be resolved with (Closure Object (), Closure Object ())
✓ named parameters work with closure type hints with ('Taylor', Closure Object ())
✓ named parameters work with registered datasets with ('Taylor', 'taylor@laravel.com')
✓ named parameters work with registered datasets with ('James', 'james@laravel.com')
✓ named parameters work with bound closure returning associative array with (Closure Object ())
✓ dataset items can mix named and sequential styles with ('Taylor', 'taylor@laravel.com')
✓ dataset items can mix named and sequential styles with ('James', 'james@laravel.com') #1
✓ dataset items can mix named and sequential styles with ('James', 'james@laravel.com') #2
PASS Tests\Features\Depends
✓ first
@ -1813,4 +1869,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, 39 todos, 35 skipped, 1211 passed (2847 assertions)
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 39 todos, 35 skipped, 1265 passed (2925 assertions)

View File

@ -0,0 +1,287 @@
<?php
/**
* Tests for dataset method chaining with hooks and describe blocks.
*
* Covers the fix from PR #1565: beforeEach()->with(), describe()->with(),
* and nested describe blocks with datasets.
*/
// ---------------------------------------------------------------
// beforeEach()->with() inside describe blocks
// ---------------------------------------------------------------
describe('beforeEach()->with() applies dataset to tests', function () {
beforeEach()->with([10]);
test('receives the dataset value', function ($value) {
expect($value)->toBe(10);
});
it('also receives the dataset value in it()', function ($value) {
expect($value)->toBe(10);
});
});
describe('beforeEach()->with() with multiple dataset values', function () {
beforeEach()->with([1, 2, 3]);
test('receives each value from the dataset', function ($value) {
expect($value)->toBeIn([1, 2, 3]);
});
});
describe('beforeEach()->with() with keyed dataset', function () {
beforeEach()->with(['first' => [10], 'second' => [20]]);
test('receives keyed dataset values', function ($value) {
expect($value)->toBeIn([10, 20]);
});
});
describe('beforeEach()->with() with closure dataset', function () {
beforeEach()->with(function () {
yield [100];
yield [200];
});
test('receives values from closure dataset', function ($value) {
expect($value)->toBeIn([100, 200]);
});
});
// ---------------------------------------------------------------
// describe()->with() method chaining
// ---------------------------------------------------------------
describe('describe()->with() passes dataset to tests', function () {
test('receives the dataset value', function ($value) {
expect($value)->toBe(42);
});
it('also receives it in it()', function ($value) {
expect($value)->toBe(42);
});
})->with([42]);
describe('describe()->with() with multiple values', function () {
test('receives each value', function ($value) {
expect($value)->toBeIn([5, 10, 15]);
});
})->with([5, 10, 15]);
describe('describe()->with() with keyed dataset', function () {
test('receives keyed values', function ($value) {
expect($value)->toBeIn([100, 200]);
});
})->with(['alpha' => [100], 'beta' => [200]]);
describe('describe()->with() with closure dataset', function () {
test('receives closure dataset values', function ($value) {
expect($value)->toBeIn([7, 14]);
});
})->with(function () {
yield [7];
yield [14];
});
// ---------------------------------------------------------------
// Nested describe blocks with datasets
// ---------------------------------------------------------------
describe('outer with dataset', function () {
describe('inner without dataset', function () {
test('inherits outer dataset', function (...$args) {
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) {
expect($args)->toBe([10]);
});
describe('level 2', function () {
test('receives datasets from all ancestor levels', function (...$args) {
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) {
expect($args)->toBe([1, 2, 3]);
});
})->with([3]);
})->with([2]);
})->with([1]);
});
// ---------------------------------------------------------------
// Combining hook datasets with test-level datasets
// ---------------------------------------------------------------
describe('beforeEach()->with() combined with test->with()', function () {
beforeEach()->with([10]);
test('receives both datasets as cross product', function ($hookValue, $testValue) {
expect($hookValue)->toBe(10);
expect($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]);
})->with([50, 60]);
})->with([5]);
// ---------------------------------------------------------------
// beforeEach()->with() combined with beforeEach closure
// ---------------------------------------------------------------
describe('beforeEach closure and beforeEach()->with() coexist', function () {
beforeEach(function () {
$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);
});
});
describe('beforeEach()->with() does not interfere with closure hooks', function () {
beforeEach(function () {
$this->counter = 1;
});
beforeEach(function () {
$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);
});
});
// ---------------------------------------------------------------
// Dataset isolation between describe blocks
// ---------------------------------------------------------------
describe('first describe with dataset', function () {
beforeEach()->with([111]);
test('gets its own dataset', function ($value) {
expect($value)->toBe(111);
});
});
describe('second describe with different dataset', function () {
beforeEach()->with([222]);
test('gets its own dataset, not the sibling', function ($value) {
expect($value)->toBe(222);
});
});
describe('third describe without dataset', function () {
test('has no dataset leaking from siblings', function () {
expect(true)->toBeTrue();
});
});
// ---------------------------------------------------------------
// describe()->with() combined with beforeEach hooks
// ---------------------------------------------------------------
describe('describe()->with() with beforeEach closure', function () {
beforeEach(function () {
$this->hookRan = true;
});
test('both hook and dataset work', function ($value) {
expect($this->hookRan)->toBeTrue();
expect($value)->toBe(77);
});
})->with([77]);
describe('describe()->with() with afterEach closure', function () {
afterEach(function () {
expect($this->value)->toBe(88);
});
test('dataset is available and afterEach runs', function ($value) {
$this->value = $value;
expect($value)->toBe(88);
});
})->with([88]);
// ---------------------------------------------------------------
// Multiple tests in a describe with beforeEach()->with()
// ---------------------------------------------------------------
describe('multiple tests share the same beforeEach dataset', function () {
beforeEach()->with([33]);
test('first test gets the dataset', function ($value) {
expect($value)->toBe(33);
});
test('second test also gets the dataset', function ($value) {
expect($value)->toBe(33);
});
it('third test with it() also gets the dataset', function ($value) {
expect($value)->toBe(33);
});
});
// ---------------------------------------------------------------
// Nested describe with beforeEach()->with() at inner level
// ---------------------------------------------------------------
describe('outer describe', function () {
beforeEach(function () {
$this->outer = true;
});
describe('inner describe with dataset on hook', function () {
beforeEach()->with([55]);
test('inherits outer beforeEach and has inner dataset', function ($value) {
expect($this->outer)->toBeTrue();
expect($value)->toBe(55);
});
});
test('outer test is unaffected by inner dataset', function () {
expect($this->outer)->toBeTrue();
});
});
// ---------------------------------------------------------------
// describe()->with() with depends
// ---------------------------------------------------------------
describe('describe()->with() preserves depends', function () {
test('first', function ($value) {
expect($value)->toBe(9);
});
test('second', function ($value) {
expect($value)->toBe(9);
})->depends('first');
})->with([9]);

View File

@ -16,7 +16,7 @@ $run = function () {
test('parallel', function () use ($run) {
expect($run('--exclude-group=integration'))
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 3 notices, 39 todos, 26 skipped, 1196 passed (2809 assertions)')
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 3 notices, 39 todos, 26 skipped, 1250 passed (2887 assertions)')
->toContain('Parallel: 3 processes');
})->skipOnWindows();