@@ -31,27 +31,69 @@ final class TestSuiteSorter
31
31
*/
32
32
public const ORDER_REVERSED = 2 ;
33
33
34
+ /**
35
+ * @var int
36
+ */
37
+ public const ORDER_DEFECTS_FIRST = 3 ;
38
+
39
+ /**
40
+ * List of sorting weights for all test result codes. A higher number gives higher priority.
41
+ */
42
+ private const DEFECT_SORT_WEIGHT = [
43
+ BaseTestRunner::STATUS_ERROR => 6 ,
44
+ BaseTestRunner::STATUS_FAILURE => 5 ,
45
+ BaseTestRunner::STATUS_WARNING => 4 ,
46
+ BaseTestRunner::STATUS_INCOMPLETE => 3 ,
47
+ BaseTestRunner::STATUS_RISKY => 2 ,
48
+ BaseTestRunner::STATUS_SKIPPED => 1 ,
49
+ BaseTestRunner::STATUS_UNKNOWN => 0
50
+ ];
51
+
52
+ /**
53
+ * @var array<string, int> Associative array of (string => DEFECT_SORT_WEIGHT) elements
54
+ */
55
+ private $ defectSortOrder = [];
56
+
57
+ /**
58
+ * @var TestResultCacheInterface
59
+ */
60
+ private $ cache ;
61
+
62
+ public function __construct (?TestResultCacheInterface $ cache = null )
63
+ {
64
+ $ this ->cache = $ cache ?? new NullTestResultCache ;
65
+ }
66
+
34
67
/**
35
68
* @throws Exception
36
69
*/
37
- public function reorderTestsInSuite (Test $ suite , int $ order , bool $ resolveDependencies ): void
70
+ public function reorderTestsInSuite (Test $ suite , int $ order , bool $ resolveDependencies, int $ orderDefects ): void
38
71
{
39
72
if ($ order !== self ::ORDER_DEFAULT && $ order !== self ::ORDER_REVERSED && $ order !== self ::ORDER_RANDOMIZED ) {
40
73
throw new Exception (
41
74
'$order must be one of TestSuiteSorter::ORDER_DEFAULT, TestSuiteSorter::ORDER_REVERSED, or TestSuiteSorter::ORDER_RANDOMIZED '
42
75
);
43
76
}
44
77
45
- if ($ suite instanceof TestSuite && !empty ($ suite ->tests ())) {
78
+ if ($ orderDefects !== self ::ORDER_DEFAULT && $ orderDefects !== self ::ORDER_DEFECTS_FIRST ) {
79
+ throw new Exception (
80
+ '$orderDefects must be one of TestSuiteSorter::ORDER_DEFAULT, TestSuiteSorter::ORDER_DEFECTS_FIRST '
81
+ );
82
+ }
83
+
84
+ if ($ suite instanceof TestSuite) {
46
85
foreach ($ suite as $ _suite ) {
47
- $ this ->reorderTestsInSuite ($ _suite , $ order , $ resolveDependencies );
86
+ $ this ->reorderTestsInSuite ($ _suite , $ order , $ resolveDependencies, $ orderDefects );
48
87
}
49
88
50
- $ this ->sort ($ suite , $ order , $ resolveDependencies );
89
+ if ($ orderDefects === self ::ORDER_DEFECTS_FIRST ) {
90
+ $ this ->addSuiteToDefectSortOrder ($ suite );
91
+ }
92
+ $ this ->sort ($ suite , $ order , $ resolveDependencies , $ orderDefects );
51
93
}
52
94
}
53
95
54
- private function sort (TestSuite $ suite , int $ order , bool $ resolveDependencies ): void
96
+ private function sort (TestSuite $ suite , int $ order , bool $ resolveDependencies, int $ orderDefects ): void
55
97
{
56
98
if (empty ($ suite ->tests ())) {
57
99
return ;
@@ -63,11 +105,29 @@ private function sort(TestSuite $suite, int $order, bool $resolveDependencies):
63
105
$ suite ->setTests ($ this ->randomize ($ suite ->tests ()));
64
106
}
65
107
108
+ if ($ orderDefects === self ::ORDER_DEFECTS_FIRST && $ this ->cache !== null ) {
109
+ $ suite ->setTests ($ this ->sortDefectsFirst ($ suite ->tests ()));
110
+ }
111
+
66
112
if ($ resolveDependencies && !($ suite instanceof DataProviderTestSuite) && $ this ->suiteOnlyContainsTests ($ suite )) {
67
113
$ suite ->setTests ($ this ->resolveDependencies ($ suite ->tests ()));
68
114
}
69
115
}
70
116
117
+ private function addSuiteToDefectSortOrder (TestSuite $ suite ): void
118
+ {
119
+ $ max = 0 ;
120
+
121
+ foreach ($ suite ->tests () as $ test ) {
122
+ if (!isset ($ this ->defectSortOrder [$ test ->getName ()])) {
123
+ $ this ->defectSortOrder [$ test ->getName ()] = self ::DEFECT_SORT_WEIGHT [$ this ->cache ->getState ($ test ->getName ())];
124
+ $ max = \max ($ max , $ this ->defectSortOrder [$ test ->getName ()]);
125
+ }
126
+ }
127
+
128
+ $ this ->defectSortOrder [$ suite ->getName ()] = $ max ;
129
+ }
130
+
71
131
private function suiteOnlyContainsTests (TestSuite $ suite ): bool
72
132
{
73
133
return \array_reduce ($ suite ->tests (), function ($ carry , $ test ) {
@@ -87,6 +147,40 @@ private function randomize(array $tests): array
87
147
return $ tests ;
88
148
}
89
149
150
+ private function sortDefectsFirst (array $ tests ): array
151
+ {
152
+ \usort ($ tests , function ($ left , $ right ) {
153
+ return $ this ->cmpDefectPriorityAndTime ($ left , $ right );
154
+ });
155
+
156
+ return $ tests ;
157
+ }
158
+
159
+ /**
160
+ * Comparator callback function to sort tests for "reach failure as fast as possible":
161
+ * 1. sort tests by defect weight defined in self::DEFECT_SORT_WEIGHT
162
+ * 2. when tests are equally defective, sort the fastest to the front
163
+ * 3. do not reorder successful tests
164
+ */
165
+ private function cmpDefectPriorityAndTime (Test $ a , Test $ b ): int
166
+ {
167
+ $ priorityA = $ this ->defectSortOrder [$ a ->getName ()] ?? 0 ;
168
+ $ priorityB = $ this ->defectSortOrder [$ b ->getName ()] ?? 0 ;
169
+
170
+ if ($ priorityB <=> $ priorityA ) {
171
+ // Sort defect weight descending
172
+ return $ priorityB <=> $ priorityA ;
173
+ }
174
+
175
+ if ($ priorityA || $ priorityB ) {
176
+ // Sort test duration ascending
177
+ return $ this ->cache ->getTime ($ a ->getName ()) <=> $ this ->cache ->getTime ($ b ->getName ());
178
+ }
179
+
180
+ // do not change execution order
181
+ return 0 ;
182
+ }
183
+
90
184
/**
91
185
* Reorder Tests within a TestCase in such a way as to resolve as many dependencies as possible.
92
186
* The algorithm will leave the tests in original running order when it can.
0 commit comments