13
13
14
14
trait SplitTestsByGroups
15
15
{
16
+ /**
17
+ * @param $numGroups
18
+ *
19
+ * @return SplitTestsByGroupsTask
20
+ */
16
21
protected function taskSplitTestsByGroups ($ numGroups )
17
22
{
18
23
return $ this ->task (SplitTestsByGroupsTask::class, $ numGroups );
19
24
}
20
25
26
+ /**
27
+ * @param $numGroups
28
+ *
29
+ * @return SplitTestFilesByGroupsTask
30
+ */
21
31
protected function taskSplitTestFilesByGroups ($ numGroups )
22
32
{
23
33
return $ this ->task (SplitTestFilesByGroupsTask::class, $ numGroups );
@@ -64,6 +74,77 @@ public function excludePath($path)
64
74
65
75
return $ this ;
66
76
}
77
+
78
+ /**
79
+ * @param $item
80
+ * @param array $items
81
+ * @param array $resolved
82
+ * @param array $unresolved
83
+ *
84
+ * @return array
85
+ */
86
+ protected function resolveDependencies ($ item , array $ items , array $ resolved , array $ unresolved ) {
87
+ $ unresolved [] = $ item ;
88
+ foreach ($ items [$ item ] as $ dep ) {
89
+ if (!in_array ($ dep , $ resolved )) {
90
+ if (!in_array ($ dep , $ unresolved )) {
91
+ $ unresolved [] = $ dep ;
92
+ list ($ resolved , $ unresolved ) = $ this ->resolveDependencies ($ dep , $ items , $ resolved , $ unresolved );
93
+ } else {
94
+ throw new \RuntimeException ("Circular dependency: $ item -> $ dep " );
95
+ }
96
+ }
97
+ }
98
+ // Add $item to $resolved if it's not already there
99
+ if (!in_array ($ item , $ resolved )) {
100
+ $ resolved [] = $ item ;
101
+ }
102
+ // Remove all occurrences of $item in $unresolved
103
+ while (($ index = array_search ($ item , $ unresolved )) !== false ) {
104
+ unset($ unresolved [$ index ]);
105
+ }
106
+
107
+ return [$ resolved , $ unresolved ];
108
+ }
109
+
110
+ /**
111
+ * Make sure that tests are in array are always with full path and name.
112
+ *
113
+ * @param array $testsListWithDependencies
114
+ *
115
+ * @return array
116
+ */
117
+ protected function resolveDependenciesToFullNames (array $ testsListWithDependencies ){
118
+ // make sure that dependencies are in array as full names
119
+ foreach ($ testsListWithDependencies as $ testName => $ test ) {
120
+ foreach ($ test as $ i => $ dependency ) {
121
+
122
+ // sometimes it is written as class::method.
123
+ // for that reason we do trim in first case and replace from :: to one in second case
124
+
125
+
126
+ // just test name, that means that class name is the same, just different method name
127
+ if (strrpos ($ dependency , ': ' ) === false ) {
128
+ $ testsListWithDependencies [$ testName ][$ i ] = trim (substr ($ testName ,0 ,strrpos ($ testName , ': ' )), ': ' ) . ': ' . $ dependency ;
129
+ continue ;
130
+ }
131
+ $ dependency = str_replace (':: ' , ': ' , $ dependency );
132
+ // className:testName, that means we need to find proper test.
133
+ list ($ targetTestFileName , $ targetTestMethodName ) = explode (': ' , $ dependency );
134
+
135
+ // look for proper test in list of all tests. Test could be in different directory so we need to compare
136
+ // strings and if matched we just assign found test name
137
+ foreach (array_keys ($ testsListWithDependencies ) as $ arrayKey ) {
138
+ if (strpos ($ arrayKey , $ targetTestFileName . '.php: ' . $ targetTestMethodName ) !== false ) {
139
+ $ testsListWithDependencies [$ testName ][$ i ] = $ arrayKey ;
140
+ continue 2 ;
141
+ }
142
+ }
143
+ throw new \RuntimeException ('Dependency target test ' .$ dependency .' not found. Please make sure test exists and you are using full test name ' );
144
+ }
145
+ }
146
+ return $ testsListWithDependencies ;
147
+ }
67
148
}
68
149
69
150
/**
@@ -89,19 +170,103 @@ public function run()
89
170
$ testLoader ->loadTests ($ this ->testsFrom );
90
171
$ tests = $ testLoader ->getTests ();
91
172
92
- $ i = 0 ;
93
- $ groups = [];
94
-
95
173
$ this ->printTaskInfo ('Processing ' . count ($ tests ) . ' tests ' );
96
- // splitting tests by groups
174
+
175
+ $ testsHaveAtLeastOneDependency = false ;
176
+
177
+ // test preloading (and fetching dependencies) requires dummy DI service.
178
+ $ di = new \Codeception \Lib \Di ();
179
+ // gather test dependencies and deal with dataproviders
180
+ $ testsListWithDependencies = [];
97
181
foreach ($ tests as $ test ) {
98
182
if ($ test instanceof DataProvider || $ test instanceof DataProviderTestSuite) {
99
183
$ test = current ($ test ->tests ());
100
184
}
101
- $ groups [($ i % $ this ->numGroups ) + 1 ][] = TestDescriptor::getTestFullName ($ test );
102
- $ i ++;
185
+
186
+ // load dependencies for cest type. Unit tests dependencies are loaded automatically
187
+ if ($ test instanceof \Codeception \Test \Cest) {
188
+ $ test ->getMetadata ()->setServices (['di ' =>$ di ]);
189
+ $ test ->preload ();
190
+ }
191
+
192
+ if (method_exists ($ test , 'getMetadata ' )) {
193
+ $ testsListWithDependencies [TestDescriptor::getTestFullName ($ test )] = $ test ->getMetadata ()
194
+ ->getDependencies ();
195
+ if ($ testsHaveAtLeastOneDependency === false and count ($ test ->getMetadata ()->getDependencies ()) != 0 ) {
196
+ $ testsHaveAtLeastOneDependency = true ;
197
+ }
198
+
199
+ // little hack to get dependencies from phpunit test cases that are private.
200
+ } elseif ($ test instanceof \PHPUnit \Framework \TestCase) {
201
+ $ ref = new \ReflectionObject ($ test );
202
+ do {
203
+ try {
204
+ $ property = $ ref ->getProperty ('dependencies ' );
205
+ $ property ->setAccessible (true );
206
+ $ testsListWithDependencies [TestDescriptor::getTestFullName ($ test )] = $ property ->getValue ($ test );
207
+
208
+ if ($ testsHaveAtLeastOneDependency === false and count ($ property ->getValue ($ test )) != 0 ) {
209
+ $ testsHaveAtLeastOneDependency = true ;
210
+ }
211
+
212
+ } catch (\ReflectionException $ e ) {
213
+ // go up on level on inheritance chain.
214
+ }
215
+ } while ($ ref = $ ref ->getParentClass ());
216
+
217
+ } else {
218
+ $ testsListWithDependencies [TestDescriptor::getTestFullName ($ test )] = [];
219
+ }
103
220
}
221
+
222
+ if ($ testsHaveAtLeastOneDependency ) {
223
+ $ this ->printTaskInfo ('Resolving test dependencies ' );
224
+
225
+ // make sure that dependencies are in array as full names
226
+ try {
227
+ $ testsListWithDependencies = $ this ->resolveDependenciesToFullNames ($ testsListWithDependencies );
228
+ } catch (\Exception $ e ) {
229
+ $ this ->printTaskError ($ e ->getMessage ());
230
+ return false ;
231
+ }
232
+
233
+ // resolved and ordered list of dependencies
234
+ $ orderedListOfTests = [];
235
+ // helper array
236
+ $ unresolved = [];
104
237
238
+ // Resolve dependencies for each test
239
+ foreach (array_keys ($ testsListWithDependencies ) as $ test ) {
240
+ try {
241
+ list ($ orderedListOfTests , $ unresolved ) = $ this ->resolveDependencies ($ test , $ testsListWithDependencies , $ orderedListOfTests , $ unresolved );
242
+ } catch (\Exception $ e ) {
243
+ $ this ->printTaskError ($ e ->getMessage ());
244
+ return false ;
245
+ }
246
+ }
247
+
248
+ // if we don't have any dependencies just use keys from original list.
249
+ } else {
250
+ $ orderedListOfTests = array_keys ($ testsListWithDependencies );
251
+ }
252
+
253
+ // for even split, calculate number of tests in each group
254
+ $ numberOfElementsInGroup = floor (count ($ orderedListOfTests ) / $ this ->numGroups );
255
+
256
+ $ i = 1 ;
257
+ $ groups = [];
258
+
259
+ // split tests into files.
260
+ foreach ($ orderedListOfTests as $ test ) {
261
+ // move to the next group ONLY if number of tests is equal or greater desired number of tests in group
262
+ // AND current test has no dependencies AKA: we are in different branch than previous test
263
+ if (!empty ($ groups [$ i ]) AND count ($ groups [$ i ]) >= $ numberOfElementsInGroup AND $ i <= ($ this ->numGroups -1 ) AND empty ($ testsListWithDependencies [$ test ])) {
264
+ $ i ++;
265
+ }
266
+
267
+ $ groups [$ i ][] = $ test ;
268
+ }
269
+
105
270
// saving group files
106
271
foreach ($ groups as $ i => $ tests ) {
107
272
$ filename = $ this ->saveTo . $ i ;
0 commit comments