Skip to content

Commit 3efa099

Browse files
Model RepeatTestSuite as a TestSuite, mirroring DataProviderTestSuite
Previously, RepeatTestSuite was declared as a leaf Test even though it structurally represents a group of test cases (the N repetitions of a test method). This produced two inconsistencies: * TestSuite::addTest() registered the suite under the first repetition's id (Class::method (repetition 1 of N)) rather than the group id, so other repetitions appeared as no members of the group. * Event\TestSuite\TestSuiteBuilder::process() treated the suite as a leaf and surfaced only tests[0]->valueObjectForEvents() into the parent's TestCollection. The collection therefore had one entry per repetition group while count() reported N: the two disagreed. Both issues are removed by treating RepeatTestSuite the same way DataProviderTestSuite is treated: * RepeatTestSuite now extends Framework\TestSuite and is constructed via RepeatTestSuite::fromTests($name, $tests, $failureThreshold). It overrides run() to retain the failure-threshold/abort semantics and delegates provides(), requires(), sortId(), and setDependencies() to its first child / all children. * A dedicated event-level value object TestSuiteForRepeatedTestMethod is introduced alongside TestSuiteForTestMethodWithDataProvider. It exposes className(), methodName(), file(), line(), and an isForRepeatedTestMethod() predicate on the base Event\TestSuite\TestSuite. * Event\TestSuite\TestSuiteBuilder::from() detects RepeatTestSuite and returns the new value object; process() recurses through it like any other framework TestSuite, so each repetition's TestMethod event value object now appears in the parent's TestCollection. * The special-case branches in Framework\TestSuite::addTest() and Runner\Filter\NameFilterIterator::accept() are removed. The inherited instanceof self branch handles registration; the existing TestSuite branch in the filter recurses into children, which then match individually. * Runner\TestResult\Collector::testSuiteFinished() learns about the new value object and, when no repetition of the method failed, records the method as passed via PassedTests::testMethodPassed(), mirroring the data-provider handler. As a consequence of RepeatTestSuite being a real TestSuite, Test Suite Started / Test Suite Finished events are now emitted around each repetition group. The JUnit XML logger correspondingly produces a nested <testsuite> element per repeated method, matching how it already renders data-provider suites.
1 parent 2b6f352 commit 3efa099

21 files changed

Lines changed: 299 additions & 89 deletions

src/Event/Value/TestSuite/TestSuite.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,12 @@ public function isForTestMethodWithDataProvider(): bool
7878
{
7979
return false;
8080
}
81+
82+
/**
83+
* @phpstan-assert-if-true TestSuiteForRepeatedTestMethod $this
84+
*/
85+
public function isForRepeatedTestMethod(): bool
86+
{
87+
return false;
88+
}
8189
}

src/Event/Value/TestSuite/TestSuiteBuilder.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use function count;
1515
use function explode;
1616
use function method_exists;
17+
use function strpos;
18+
use function substr;
1719
use PHPUnit\Event\Code\Test;
1820
use PHPUnit\Event\Code\TestCollection;
1921
use PHPUnit\Event\RuntimeException;
@@ -67,6 +69,44 @@ public static function from(FrameworkTestSuite $testSuite): TestSuite
6769
);
6870
}
6971

72+
if ($testSuite instanceof RepeatTestSuite) {
73+
$name = $testSuite->name();
74+
75+
$separatorPosition = strpos($name, '::');
76+
77+
assert($separatorPosition !== false);
78+
79+
$className = substr($name, 0, $separatorPosition);
80+
$methodName = substr($name, $separatorPosition + 2);
81+
82+
$hashPosition = strpos($methodName, '#');
83+
84+
if ($hashPosition !== false) {
85+
$methodName = substr($methodName, 0, $hashPosition);
86+
}
87+
88+
assert($className !== '' && class_exists($className));
89+
assert($methodName !== '' && method_exists($className, $methodName));
90+
91+
$reflector = new ReflectionMethod($className, $methodName);
92+
93+
$file = $reflector->getFileName();
94+
$line = $reflector->getStartLine();
95+
96+
assert($file !== false);
97+
assert($line !== false);
98+
99+
return new TestSuiteForRepeatedTestMethod(
100+
$name,
101+
$testSuite->count(),
102+
TestCollection::fromArray($tests),
103+
$className,
104+
$methodName,
105+
$file,
106+
$line,
107+
);
108+
}
109+
70110
if ($testSuite->isForTestClass()) {
71111
$testClassName = $testSuite->name();
72112

@@ -108,12 +148,6 @@ private static function process(FrameworkTestSuite $testSuite, array &$tests): v
108148
continue;
109149
}
110150

111-
if ($test instanceof RepeatTestSuite) {
112-
$tests[] = $test->valueObjectForEvents();
113-
114-
continue;
115-
}
116-
117151
if ($test instanceof TestCase || $test instanceof PhptTestCase) {
118152
$tests[] = $test->valueObjectForEvents();
119153
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\Event\TestSuite;
11+
12+
use PHPUnit\Event\Code\TestCollection;
13+
14+
/**
15+
* @immutable
16+
*
17+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
18+
*/
19+
final readonly class TestSuiteForRepeatedTestMethod extends TestSuite
20+
{
21+
/**
22+
* @var class-string
23+
*/
24+
private string $className;
25+
26+
/**
27+
* @var non-empty-string
28+
*/
29+
private string $methodName;
30+
private string $file;
31+
private int $line;
32+
33+
/**
34+
* @param non-empty-string $name
35+
* @param class-string $className
36+
* @param non-empty-string $methodName
37+
*/
38+
public function __construct(string $name, int $size, TestCollection $tests, string $className, string $methodName, string $file, int $line)
39+
{
40+
parent::__construct($name, $size, $tests);
41+
42+
$this->className = $className;
43+
$this->methodName = $methodName;
44+
$this->file = $file;
45+
$this->line = $line;
46+
}
47+
48+
/**
49+
* @return class-string
50+
*/
51+
public function className(): string
52+
{
53+
return $this->className;
54+
}
55+
56+
/**
57+
* @return non-empty-string
58+
*/
59+
public function methodName(): string
60+
{
61+
return $this->methodName;
62+
}
63+
64+
public function file(): string
65+
{
66+
return $this->file;
67+
}
68+
69+
public function line(): int
70+
{
71+
return $this->line;
72+
}
73+
74+
public function isForRepeatedTestMethod(): true
75+
{
76+
return true;
77+
}
78+
}

src/Framework/RepeatTestSuite.php

Lines changed: 69 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,54 +9,73 @@
99
*/
1010
namespace PHPUnit\Framework;
1111

12-
use function count;
12+
use function assert;
1313
use PHPUnit\Event;
14-
use PHPUnit\Event\Code\TestMethod;
1514
use PHPUnit\Event\NoPreviousThrowableException;
15+
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
16+
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
17+
use SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException;
1618

1719
/**
1820
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
1921
*
2022
* @internal This class is not covered by the backward compatibility promise for PHPUnit
2123
*/
22-
final class RepeatTestSuite implements Reorderable, SelfDescribing, Test
24+
final class RepeatTestSuite extends TestSuite
2325
{
24-
/**
25-
* @var non-empty-list<TestCase>
26-
*/
27-
private array $tests;
28-
2926
/**
3027
* @var positive-int
3128
*/
32-
private int $failureThreshold;
29+
private int $failureThreshold = 1;
3330

3431
/**
32+
* @param non-empty-string $name
3533
* @param non-empty-list<TestCase> $tests
3634
* @param positive-int $failureThreshold
3735
*/
38-
public function __construct(array $tests, int $failureThreshold = 1)
36+
public static function fromTests(string $name, array $tests, int $failureThreshold): self
3937
{
40-
$this->tests = $tests;
41-
$this->failureThreshold = $failureThreshold;
42-
}
38+
$suite = self::empty($name);
4339

44-
public function count(): int
45-
{
46-
return count($this->tests);
40+
$suite->failureThreshold = $failureThreshold;
41+
42+
foreach ($tests as $test) {
43+
$suite->addTest($test);
44+
}
45+
46+
return $suite;
4747
}
4848

4949
/**
50-
* @throws Event\InvalidArgumentException
50+
* @throws Event\RuntimeException
5151
* @throws Exception
52+
* @throws InvalidArgumentException
5253
* @throws NoPreviousThrowableException
54+
* @throws UnintentionallyCoveredCodeException
5355
*/
5456
public function run(): void
5557
{
58+
if ($this->isEmpty()) {
59+
return;
60+
}
61+
62+
$emitter = Event\Facade::emitter();
63+
$testSuiteValueObjectForEvents = Event\TestSuite\TestSuiteBuilder::from($this);
64+
65+
$emitter->testSuiteStarted($testSuiteValueObjectForEvents);
66+
5667
$failureCount = 0;
5768
$lastFailedRepetition = 0;
5869

59-
foreach ($this->tests as $test) {
70+
foreach ($this as $test) {
71+
assert($test instanceof TestCase);
72+
73+
if (TestResultFacade::shouldStop()) {
74+
$emitter->testRunnerExecutionAborted();
75+
76+
break;
77+
}
78+
6079
if ($failureCount >= $this->failureThreshold) {
6180
$test->markSkippedForRepeatAbort($lastFailedRepetition);
6281

@@ -70,57 +89,61 @@ public function run(): void
7089
$lastFailedRepetition = $test->repetition();
7190
}
7291
}
92+
93+
$emitter->testSuiteFinished($testSuiteValueObjectForEvents);
7394
}
7495

75-
public function sortId(): string
96+
/**
97+
* @param list<ExecutionOrderDependency> $dependencies
98+
*/
99+
public function setDependencies(array $dependencies): void
76100
{
77-
return $this->tests[0]->sortId();
101+
foreach ($this->tests() as $test) {
102+
assert($test instanceof TestCase);
103+
104+
$test->setDependencies($dependencies);
105+
}
78106
}
79107

80108
/**
81109
* @return list<ExecutionOrderDependency>
82110
*/
83111
public function provides(): array
84112
{
85-
return $this->tests[0]->provides();
113+
$tests = $this->tests();
114+
115+
if ($tests === []) {
116+
return [];
117+
}
118+
119+
assert($tests[0] instanceof TestCase);
120+
121+
return $tests[0]->provides();
86122
}
87123

88124
/**
89125
* @return list<ExecutionOrderDependency>
90126
*/
91127
public function requires(): array
92128
{
93-
return $this->tests[0]->requires();
94-
}
129+
$tests = $this->tests();
95130

96-
/**
97-
* @param list<ExecutionOrderDependency> $dependencies
98-
*/
99-
public function setDependencies(array $dependencies): void
100-
{
101-
foreach ($this->tests as $test) {
102-
$test->setDependencies($dependencies);
131+
if ($tests === []) {
132+
return [];
103133
}
104-
}
105134

106-
/**
107-
* @return non-empty-string
108-
*/
109-
public function name(): string
110-
{
111-
return $this->tests[0]::class . '::' . $this->tests[0]->nameWithDataSet();
112-
}
135+
assert($tests[0] instanceof TestCase);
113136

114-
/**
115-
* @return non-empty-string
116-
*/
117-
public function toString(): string
118-
{
119-
return $this->name();
137+
return $tests[0]->requires();
120138
}
121139

122-
public function valueObjectForEvents(): TestMethod
140+
public function sortId(): string
123141
{
124-
return $this->tests[0]->valueObjectForEvents();
142+
$tests = $this->tests();
143+
144+
assert($tests !== []);
145+
assert($tests[0] instanceof TestCase);
146+
147+
return $tests[0]->sortId();
125148
}
126149
}

src/Framework/TestBuilder.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,11 @@ private function buildDataProviderTestSuite(string $methodName, string $classNam
192192
}
193193

194194
$dataProviderTestSuite->addTest(
195-
new RepeatTestSuite($tests, $failureThreshold),
195+
RepeatTestSuite::fromTests(
196+
$className . '::' . $methodName . '#' . $_dataName,
197+
$tests,
198+
$failureThreshold,
199+
),
196200
$groups,
197201
);
198202
} else {
@@ -240,7 +244,11 @@ private function buildRepeatTestSuite(string $className, string $methodName, int
240244
$tests[] = $test;
241245
}
242246

243-
return new RepeatTestSuite($tests, $failureThreshold);
247+
return RepeatTestSuite::fromTests(
248+
$className . '::' . $methodName,
249+
$tests,
250+
$failureThreshold,
251+
);
244252
}
245253

246254
/**

src/Framework/TestSuite.php

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -158,28 +158,6 @@ public function addTest(Test $test, array $groups = []): void
158158
return;
159159
}
160160

161-
if ($test instanceof RepeatTestSuite) {
162-
$this->tests[] = $test;
163-
164-
$this->clearCaches();
165-
166-
$id = $test->valueObjectForEvents()->id();
167-
168-
if ($this->containsOnlyVirtualGroups($groups)) {
169-
$groups[] = 'default';
170-
}
171-
172-
foreach ($groups as $group) {
173-
if (!isset($this->groups[$group])) {
174-
$this->groups[$group] = [$id];
175-
} else {
176-
$this->groups[$group][] = $id;
177-
}
178-
}
179-
180-
return;
181-
}
182-
183161
assert($test instanceof TestCase || $test instanceof PhptTestCase);
184162

185163
$this->tests[] = $test;

0 commit comments

Comments
 (0)