1
|
|
|
<?php |
2
|
|
|
namespace ParaTest\Runners\PHPUnit; |
3
|
|
|
|
4
|
|
|
use ParaTest\Parser\NoClassInFileException; |
5
|
|
|
use ParaTest\Parser\ParsedClass; |
6
|
|
|
use ParaTest\Parser\ParsedObject; |
7
|
|
|
use ParaTest\Parser\Parser; |
8
|
|
|
|
9
|
|
|
class SuiteLoader |
10
|
|
|
{ |
11
|
|
|
/** |
12
|
|
|
* The collection of loaded files |
13
|
|
|
* |
14
|
|
|
* @var array |
15
|
|
|
*/ |
16
|
|
|
protected $files = array(); |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* The collection of parsed test classes |
20
|
|
|
* |
21
|
|
|
* @var array |
22
|
|
|
*/ |
23
|
|
|
protected $loadedSuites = array(); |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var Options |
27
|
|
|
*/ |
28
|
|
|
private $options; |
29
|
|
|
|
30
|
26 |
|
public function __construct($options = null) |
31
|
|
|
{ |
32
|
26 |
|
if ($options && !$options instanceof Options) { |
33
|
1 |
|
throw new \InvalidArgumentException("SuiteLoader options must be null or of type Options"); |
34
|
|
|
} |
35
|
|
|
|
36
|
25 |
|
$this->options = $options; |
37
|
25 |
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Returns all parsed suite objects as ExecutableTest |
41
|
|
|
* instances |
42
|
|
|
* |
43
|
|
|
* @return array |
44
|
|
|
*/ |
45
|
2 |
|
public function getSuites() |
46
|
|
|
{ |
47
|
2 |
|
return $this->loadedSuites; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Returns a collection of TestMethod objects |
52
|
|
|
* for all loaded ExecutableTest instances |
53
|
|
|
* |
54
|
|
|
* @return array |
55
|
|
|
*/ |
56
|
2 |
|
public function getTestMethods() |
57
|
|
|
{ |
58
|
2 |
|
$methods = array(); |
59
|
2 |
|
foreach ($this->loadedSuites as $suite) { |
60
|
2 |
|
$methods = array_merge($methods, $suite->getFunctions()); |
61
|
2 |
|
} |
62
|
|
|
|
63
|
2 |
|
return $methods; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Populates the loaded suite collection. Will load suites |
68
|
|
|
* based off a phpunit xml configuration or a specified path |
69
|
|
|
* |
70
|
|
|
* @param string $path |
71
|
|
|
* @throws \RuntimeException |
72
|
|
|
*/ |
73
|
23 |
|
public function load($path = '') |
74
|
|
|
{ |
75
|
23 |
|
if (is_object($this->options) && isset($this->options->filtered['configuration'])) { |
76
|
16 |
|
$configuration = $this->options->filtered['configuration']; |
77
|
16 |
|
} else { |
78
|
7 |
|
$configuration = new Configuration(''); |
79
|
|
|
} |
80
|
|
|
|
81
|
23 |
|
if ($path) { |
82
|
9 |
|
$testFileLoader = new TestFileLoader($this->options); |
83
|
9 |
|
$this->files = array_merge($this->files, $testFileLoader->loadPath($path)); |
84
|
22 |
|
} elseif (isset($this->options->testsuite) && $this->options->testsuite) { |
85
|
13 |
|
foreach ($configuration->getSuiteByName($this->options->testsuite) as $suite) { |
86
|
|
|
foreach ($suite as $suitePath) { |
87
|
|
|
$testFileLoader = new TestFileLoader($this->options); |
88
|
|
|
$this->files = array_merge($this->files, $testFileLoader->loadSuitePath($suitePath)); |
89
|
|
|
} |
90
|
|
|
} |
91
|
14 |
|
} elseif ($suites = $configuration->getSuites()) { |
92
|
12 |
|
foreach ($suites as $suite) { |
93
|
12 |
|
foreach ($suite as $suitePath) { |
94
|
12 |
|
$testFileLoader = new TestFileLoader($this->options); |
95
|
12 |
|
$this->files = array_merge($this->files, $testFileLoader->loadSuitePath($suitePath)); |
96
|
12 |
|
} |
97
|
12 |
|
} |
98
|
12 |
|
} |
99
|
|
|
|
100
|
|
|
|
101
|
21 |
|
if (!$this->files) { |
|
|
|
|
102
|
1 |
|
throw new \RuntimeException("No path or configuration provided (tests must end with Test.php)"); |
103
|
|
|
} |
104
|
|
|
|
105
|
20 |
|
$this->files = array_unique($this->files); // remove duplicates |
106
|
|
|
|
107
|
20 |
|
$this->initSuites(); |
108
|
20 |
|
} |
109
|
|
|
|
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Called after all files are loaded. Parses loaded files into |
113
|
|
|
* ExecutableTest objects - either Suite or TestMethod |
114
|
|
|
*/ |
115
|
20 |
|
private function initSuites() |
116
|
|
|
{ |
117
|
20 |
|
foreach ($this->files as $path) { |
118
|
|
|
try { |
119
|
20 |
|
$parser = new Parser($path); |
120
|
19 |
|
if ($class = $parser->getClass()) { |
121
|
19 |
|
$this->loadedSuites[$path] = $this->createSuite($path, $class); |
122
|
19 |
|
} |
123
|
20 |
|
} catch (NoClassInFileException $e) { |
124
|
1 |
|
continue; |
125
|
|
|
} |
126
|
20 |
|
} |
127
|
20 |
|
} |
128
|
|
|
|
129
|
19 |
|
private function executableTests($path, $class) |
130
|
|
|
{ |
131
|
19 |
|
$executableTests = array(); |
132
|
19 |
|
$methodBatches = $this->getMethodBatches($class); |
133
|
19 |
|
foreach ($methodBatches as $methodBatch) { |
134
|
19 |
|
$executableTest = new TestMethod($path, $methodBatch); |
135
|
19 |
|
$executableTests[] = $executableTest; |
136
|
19 |
|
} |
137
|
19 |
|
return $executableTests; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Get method batches. |
142
|
|
|
* |
143
|
|
|
* Identify method dependencies, and group dependents and dependees on a single methodBatch. |
144
|
|
|
* Use max batch size to fill batches. |
145
|
|
|
* |
146
|
|
|
* @param ParsedClass $class |
147
|
|
|
* @return array of MethodBatches. Each MethodBatch has an array of method names |
148
|
|
|
*/ |
149
|
19 |
|
private function getMethodBatches($class) |
150
|
|
|
{ |
151
|
19 |
|
$classMethods = $class->getMethods($this->options ? $this->options->annotations : array()); |
|
|
|
|
152
|
19 |
|
$maxBatchSize = $this->options && $this->options->functional ? $this->options->maxBatchSize : 0; |
153
|
19 |
|
$batches = array(); |
154
|
19 |
|
foreach ($classMethods as $method) { |
155
|
19 |
|
$tests = $this->getMethodTests($class, $method, $maxBatchSize != 0); |
156
|
|
|
// if filter passed to paratest then method tests can be blank if not match to filter |
157
|
19 |
|
if (!$tests) { |
|
|
|
|
158
|
|
|
continue; |
159
|
|
|
} |
160
|
|
|
|
161
|
19 |
|
if (($dependsOn = $this->methodDependency($method)) != null) { |
|
|
|
|
162
|
1 |
|
$this->addDependentTestsToBatchSet($batches, $dependsOn, $tests); |
163
|
1 |
|
} else { |
164
|
19 |
|
$this->addTestsToBatchSet($batches, $tests, $maxBatchSize); |
165
|
|
|
} |
166
|
19 |
|
} |
167
|
19 |
|
return $batches; |
168
|
|
|
} |
169
|
|
|
|
170
|
1 |
|
private function addDependentTestsToBatchSet(&$batches, $dependsOn, $tests) |
171
|
|
|
{ |
172
|
1 |
|
foreach ($batches as $key => $batch) { |
173
|
1 |
|
foreach ($batch as $methodName) { |
174
|
1 |
|
if ($dependsOn === $methodName) { |
175
|
1 |
|
$batches[$key] = array_merge($batches[$key], $tests); |
176
|
1 |
|
continue; |
177
|
|
|
} |
178
|
1 |
|
} |
179
|
1 |
|
} |
180
|
1 |
|
} |
181
|
|
|
|
182
|
19 |
|
private function addTestsToBatchSet(&$batches, $tests, $maxBatchSize) |
183
|
|
|
{ |
184
|
19 |
|
foreach ($tests as $test) { |
185
|
19 |
|
$lastIndex = count($batches) - 1; |
186
|
|
|
if ($lastIndex != -1 |
187
|
19 |
|
&& count($batches[$lastIndex]) < $maxBatchSize |
188
|
19 |
|
) { |
189
|
|
|
$batches[$lastIndex][] = $test; |
190
|
|
|
} else { |
191
|
19 |
|
$batches[] = array($test); |
192
|
|
|
} |
193
|
19 |
|
} |
194
|
19 |
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Get method all available tests. |
198
|
|
|
* |
199
|
|
|
* With empty filter this method returns single test if doesnt' have data provider or |
200
|
|
|
* data provider is not used and return all test if has data provider and data provider is used. |
201
|
|
|
* |
202
|
|
|
* @param ParsedClass $class Parsed class. |
203
|
|
|
* @param ParsedObject $method Parsed method. |
204
|
|
|
* @param bool $useDataProvider Try to use data provider or not. |
205
|
|
|
* @return string[] Array of test names. |
206
|
|
|
*/ |
207
|
19 |
|
private function getMethodTests($class, $method, $useDataProvider = false) |
208
|
|
|
{ |
209
|
19 |
|
$result = array(); |
210
|
|
|
|
211
|
19 |
|
$groups = $this->methodGroups($method); |
212
|
|
|
|
213
|
19 |
|
$dataProvider = $this->methodDataProvider($method); |
214
|
19 |
|
if ($useDataProvider && isset($dataProvider)) { |
215
|
|
|
$testFullClassName = "\\" . $class->getName(); |
216
|
|
|
$testClass = new $testFullClassName(); |
217
|
|
|
$result = array(); |
218
|
|
|
$datasetKeys = array_keys($testClass->$dataProvider()); |
219
|
|
|
foreach ($datasetKeys as $key) { |
220
|
|
|
$test = sprintf( |
221
|
|
|
"%s with data set %s", |
222
|
|
|
$method->getName(), |
223
|
|
|
is_int($key) ? "#" . $key : "\"" . $key . "\"" |
224
|
|
|
); |
225
|
|
|
if ($this->testMatchOptions($class->getName(), $test, $groups)) { |
226
|
|
|
$result[] = $test; |
227
|
|
|
} |
228
|
|
|
} |
229
|
19 |
|
} elseif ($this->testMatchOptions($class->getName(), $method->getName(), $groups)) { |
230
|
19 |
|
$result = array($method->getName()); |
231
|
19 |
|
} |
232
|
|
|
|
233
|
19 |
|
return $result; |
234
|
|
|
} |
235
|
|
|
|
236
|
19 |
|
private function testMatchGroupOptions($groups) |
237
|
|
|
{ |
238
|
19 |
|
if (empty($groups)) { |
239
|
17 |
|
return true; |
240
|
|
|
} |
241
|
|
|
|
242
|
15 |
|
if (!empty($this->options->groups) |
243
|
15 |
|
&& !array_intersect($groups, $this->options->groups) |
244
|
15 |
|
) { |
245
|
|
|
return false; |
246
|
|
|
} |
247
|
|
|
|
248
|
15 |
|
if (!empty($this->options->excludeGroups) |
249
|
15 |
|
&& array_intersect($groups, $this->options->excludeGroups) |
250
|
15 |
|
) { |
251
|
|
|
return false; |
252
|
|
|
} |
253
|
|
|
|
254
|
15 |
|
return true; |
255
|
|
|
} |
256
|
|
|
|
257
|
19 |
|
private function testMatchFilterOptions($className, $name, $group) |
|
|
|
|
258
|
|
|
{ |
259
|
19 |
|
if (empty($this->options->filter)) { |
|
|
|
|
260
|
19 |
|
return true; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
$re = substr($this->options->filter, 0, 1) == "/" |
|
|
|
|
264
|
|
|
? $this->options->filter |
|
|
|
|
265
|
|
|
: "/" . $this->options->filter . "/"; |
|
|
|
|
266
|
|
|
$fullName = $className . "::" . $name; |
267
|
|
|
$result = preg_match($re, $fullName); |
268
|
|
|
|
269
|
|
|
return $result; |
270
|
|
|
} |
271
|
|
|
|
272
|
19 |
|
private function testMatchOptions($className, $name, $group) |
273
|
|
|
{ |
274
|
19 |
|
$result = $this->testMatchGroupOptions($group) |
275
|
19 |
|
&& $this->testMatchFilterOptions($className, $name, $group); |
276
|
|
|
|
277
|
19 |
|
return $result; |
278
|
|
|
} |
279
|
|
|
|
280
|
19 |
|
private function methodDataProvider($method) |
281
|
|
|
{ |
282
|
19 |
|
if (preg_match("/@\bdataProvider\b \b(.*)\b/", $method->getDocBlock(), $matches)) { |
283
|
|
|
return $matches[1]; |
284
|
|
|
} |
285
|
19 |
|
return null; |
286
|
|
|
} |
287
|
|
|
|
288
|
19 |
|
private function methodDependency($method) |
289
|
|
|
{ |
290
|
19 |
|
if (preg_match("/@\bdepends\b \b(.*)\b/", $method->getDocBlock(), $matches)) { |
291
|
1 |
|
return $matches[1]; |
292
|
|
|
} |
293
|
19 |
|
return null; |
294
|
|
|
} |
295
|
|
|
|
296
|
19 |
|
private function methodGroups($method) |
297
|
|
|
{ |
298
|
19 |
|
if (preg_match_all("/@\bgroup\b \b(.*)\b/", $method->getDocBlock(), $matches)) { |
299
|
15 |
|
return $matches[1]; |
300
|
|
|
} |
301
|
17 |
|
return array(); |
302
|
|
|
} |
303
|
|
|
|
304
|
19 |
|
private function createSuite($path, ParsedClass $class) |
305
|
|
|
{ |
306
|
19 |
|
return new Suite( |
307
|
19 |
|
$path, |
308
|
19 |
|
$this->executableTests( |
309
|
19 |
|
$path, |
310
|
|
|
$class |
311
|
19 |
|
), |
312
|
19 |
|
$class->getName() |
313
|
19 |
|
); |
314
|
|
|
} |
315
|
|
|
} |
316
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.