Skip to content

Commit 102aef2

Browse files
Initial work repeated test execution
1 parent e85b58e commit 102aef2

29 files changed

Lines changed: 898 additions & 78 deletions

src/Event/Value/Test/TestMethod.php

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,36 @@
3939
private MetadataCollection $metadata;
4040
private TestDataCollection $testData;
4141

42+
/**
43+
* @var positive-int
44+
*/
45+
private int $repetition;
46+
47+
/**
48+
* @var positive-int
49+
*/
50+
private int $totalRepetitions;
51+
4252
/**
4353
* @param class-string $className
4454
* @param non-empty-string $methodName
4555
* @param non-empty-string $file
4656
* @param non-negative-int $line
57+
* @param positive-int $repetition
58+
* @param positive-int $totalRepetitions
4759
*/
48-
public function __construct(string $className, string $methodName, string $file, int $line, TestDox $testDox, MetadataCollection $metadata, TestDataCollection $testData)
60+
public function __construct(string $className, string $methodName, string $file, int $line, TestDox $testDox, MetadataCollection $metadata, TestDataCollection $testData, int $repetition = 1, int $totalRepetitions = 1)
4961
{
5062
parent::__construct($file);
5163

52-
$this->className = $className;
53-
$this->methodName = $methodName;
54-
$this->line = $line;
55-
$this->testDox = $testDox;
56-
$this->metadata = $metadata;
57-
$this->testData = $testData;
64+
$this->className = $className;
65+
$this->methodName = $methodName;
66+
$this->line = $line;
67+
$this->testDox = $testDox;
68+
$this->metadata = $metadata;
69+
$this->testData = $testData;
70+
$this->repetition = $repetition;
71+
$this->totalRepetitions = $totalRepetitions;
5872
}
5973

6074
/**
@@ -96,6 +110,27 @@ public function testData(): TestDataCollection
96110
return $this->testData;
97111
}
98112

113+
/**
114+
* @return positive-int
115+
*/
116+
public function repetition(): int
117+
{
118+
return $this->repetition;
119+
}
120+
121+
/**
122+
* @return positive-int
123+
*/
124+
public function totalRepetitions(): int
125+
{
126+
return $this->totalRepetitions;
127+
}
128+
129+
public function isRepeated(): bool
130+
{
131+
return $this->totalRepetitions > 1;
132+
}
133+
99134
public function isTestMethod(): true
100135
{
101136
return true;
@@ -112,6 +147,14 @@ public function id(): string
112147
$buffer .= '#' . $this->testData->dataFromDataProvider()->dataSetName();
113148
}
114149

150+
if ($this->totalRepetitions > 1) {
151+
$buffer .= sprintf(
152+
' (repetition %d of %d)',
153+
$this->repetition,
154+
$this->totalRepetitions,
155+
);
156+
}
157+
115158
return $buffer;
116159
}
117160

@@ -128,24 +171,32 @@ public function nameWithClass(): string
128171
*/
129172
public function name(): string
130173
{
131-
if (!$this->testData->hasDataFromDataProvider()) {
132-
return $this->methodName;
174+
$name = $this->methodName;
175+
176+
if ($this->testData->hasDataFromDataProvider()) {
177+
$dataSetName = $this->testData->dataFromDataProvider()->dataSetName();
178+
179+
if (is_int($dataSetName)) {
180+
$name .= sprintf(
181+
' with data set #%d',
182+
$dataSetName,
183+
);
184+
} else {
185+
$name .= sprintf(
186+
' with data set "%s"',
187+
$dataSetName,
188+
);
189+
}
133190
}
134191

135-
$dataSetName = $this->testData->dataFromDataProvider()->dataSetName();
136-
137-
if (is_int($dataSetName)) {
138-
$dataSetName = sprintf(
139-
' with data set #%d',
140-
$dataSetName,
141-
);
142-
} else {
143-
$dataSetName = sprintf(
144-
' with data set "%s"',
145-
$dataSetName,
192+
if ($this->totalRepetitions > 1) {
193+
$name .= sprintf(
194+
' (repetition %d of %d)',
195+
$this->repetition,
196+
$this->totalRepetitions,
146197
);
147198
}
148199

149-
return $this->methodName . $dataSetName;
200+
return $name;
150201
}
151202
}

src/Event/Value/Test/TestMethodBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public static function fromTestCase(TestCase $testCase, bool $useTestCaseForTest
4545
$testDox,
4646
MetadataRegistry::parser()->forClassAndMethod($testCase::class, $methodName),
4747
self::dataFor($testCase),
48+
$testCase->repetition(),
49+
$testCase->totalRepetitions(),
4850
);
4951
}
5052

src/Event/Value/TestSuite/TestSuiteBuilder.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use PHPUnit\Event\Code\TestCollection;
1919
use PHPUnit\Event\RuntimeException;
2020
use PHPUnit\Framework\DataProviderTestSuite;
21+
use PHPUnit\Framework\RepeatTestSuite;
2122
use PHPUnit\Framework\TestCase;
2223
use PHPUnit\Framework\TestSuite as FrameworkTestSuite;
2324
use PHPUnit\Runner\Phpt\TestCase as PhptTestCase;
@@ -107,6 +108,12 @@ private static function process(FrameworkTestSuite $testSuite, array &$tests): v
107108
continue;
108109
}
109110

111+
if ($test instanceof RepeatTestSuite) {
112+
$tests[] = $test->valueObjectForEvents();
113+
114+
continue;
115+
}
116+
110117
if ($test instanceof TestCase || $test instanceof PhptTestCase) {
111118
$tests[] = $test->valueObjectForEvents();
112119
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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\Framework\Attributes;
11+
12+
use Attribute;
13+
14+
/**
15+
* @immutable
16+
*
17+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
18+
*/
19+
#[Attribute(Attribute::TARGET_METHOD)]
20+
final readonly class Repeat
21+
{
22+
/**
23+
* @var positive-int
24+
*/
25+
private int $times;
26+
27+
/**
28+
* @var positive-int
29+
*/
30+
private int $failureThreshold;
31+
32+
/**
33+
* @param positive-int $times
34+
* @param positive-int $failureThreshold
35+
*/
36+
public function __construct(int $times, int $failureThreshold = 1)
37+
{
38+
$this->times = $times;
39+
$this->failureThreshold = $failureThreshold;
40+
}
41+
42+
/**
43+
* @return positive-int
44+
*/
45+
public function times(): int
46+
{
47+
return $this->times;
48+
}
49+
50+
/**
51+
* @return positive-int
52+
*/
53+
public function failureThreshold(): int
54+
{
55+
return $this->failureThreshold;
56+
}
57+
}

src/Framework/RepeatTestSuite.php

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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\Framework;
11+
12+
use function count;
13+
use PHPUnit\Event;
14+
use PHPUnit\Event\Code\TestMethod;
15+
use PHPUnit\Event\NoPreviousThrowableException;
16+
17+
/**
18+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
19+
*
20+
* @internal This class is not covered by the backward compatibility promise for PHPUnit
21+
*/
22+
final class RepeatTestSuite implements Reorderable, SelfDescribing, Test
23+
{
24+
/**
25+
* @var non-empty-list<TestCase>
26+
*/
27+
private array $tests;
28+
29+
/**
30+
* @var positive-int
31+
*/
32+
private int $failureThreshold;
33+
34+
/**
35+
* @param non-empty-list<TestCase> $tests
36+
* @param positive-int $failureThreshold
37+
*/
38+
public function __construct(array $tests, int $failureThreshold = 1)
39+
{
40+
$this->tests = $tests;
41+
$this->failureThreshold = $failureThreshold;
42+
}
43+
44+
public function count(): int
45+
{
46+
return count($this->tests);
47+
}
48+
49+
/**
50+
* @throws Event\InvalidArgumentException
51+
* @throws Exception
52+
* @throws NoPreviousThrowableException
53+
*/
54+
public function run(): void
55+
{
56+
$failureCount = 0;
57+
$lastFailedRepetition = 0;
58+
59+
foreach ($this->tests as $test) {
60+
if ($failureCount >= $this->failureThreshold) {
61+
$test->markSkippedForRepeatAbort($lastFailedRepetition);
62+
63+
continue;
64+
}
65+
66+
$test->run();
67+
68+
if ($test->status()->isFailure() || $test->status()->isError()) {
69+
$failureCount++;
70+
$lastFailedRepetition = $test->repetition();
71+
}
72+
}
73+
}
74+
75+
public function sortId(): string
76+
{
77+
return $this->tests[0]->sortId();
78+
}
79+
80+
/**
81+
* @return list<ExecutionOrderDependency>
82+
*/
83+
public function provides(): array
84+
{
85+
return $this->tests[0]->provides();
86+
}
87+
88+
/**
89+
* @return list<ExecutionOrderDependency>
90+
*/
91+
public function requires(): array
92+
{
93+
return $this->tests[0]->requires();
94+
}
95+
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);
103+
}
104+
}
105+
106+
public function name(): string
107+
{
108+
return $this->tests[0]::class . '::' . $this->tests[0]->nameWithDataSet();
109+
}
110+
111+
/**
112+
* @return non-empty-string
113+
*/
114+
public function toString(): string
115+
{
116+
return $this->name();
117+
}
118+
119+
public function valueObjectForEvents(): TestMethod
120+
{
121+
return $this->tests[0]->valueObjectForEvents();
122+
}
123+
}

0 commit comments

Comments
 (0)