Completed
Pull Request — master (#192)
by
unknown
02:29
created

SuiteLoader::createSuite()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0117

Importance

Changes 3
Bugs 2 Features 0
Metric Value
c 3
b 2
f 0
dl 0
loc 13
ccs 6
cts 7
cp 0.8571
rs 9.4286
cc 2
eloc 7
nc 2
nop 2
crap 2.0117
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 pattern used for grabbing test files. Uses the *Test.php convention
13
     * that PHPUnit defaults to.
14
     */
15
    const TEST_PATTERN = '/.+Test\.php$/';
16
17
    /**
18
     * Matches php files
19
     */
20
    const FILE_PATTERN = '/.+\.php$/';
21
22
    /**
23
     * The collection of loaded files
24
     *
25
     * @var array
26
     */
27
    protected $files = array();
28
29
    /**
30
     * The collection of parsed test classes
31
     *
32
     * @var array
33
     */
34
    protected $loadedSuites = array();
35
36
    /**
37
     * Used to ignore directory paths '.' and '..'
38
     *
39
     * @var string
40
     */
41
    private static $dotPattern = '/([.]+)$/';
42
43 23
    public function __construct($options = null)
44
    {
45 23
        if ($options && !$options instanceof Options) {
46 1
            throw new \InvalidArgumentException("SuiteLoader options must be null or of type Options");
47
        }
48
49 22
        $this->options = $options;
0 ignored issues
show
Bug introduced by
The property options does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
50 22
    }
51
52
    /**
53
     * Returns all parsed suite objects as ExecutableTest
54
     * instances
55
     *
56
     * @return array
57
     */
58 2
    public function getSuites()
59
    {
60 2
        return $this->loadedSuites;
61
    }
62
63
    /**
64
     * Returns a collection of TestMethod objects
65
     * for all loaded ExecutableTest instances
66
     *
67
     * @return array
68
     */
69 1
    public function getTestMethods()
70
    {
71 1
        $methods = array();
72 1
        foreach ($this->loadedSuites as $suite) {
73 1
            $methods = array_merge($methods, $suite->getFunctions());
74 1
        }
75
76 1
        return $methods;
77
    }
78
79
    /**
80
     * Populates the loaded suite collection. Will load suites
81
     * based off a phpunit xml configuration or a specified path
82
     *
83
     * @param string $path
84
     * @throws \RuntimeException
85
     */
86 20
    public function load($path = '')
87
    {
88 20
        if (is_object($this->options) && isset($this->options->filtered['configuration'])) {
89 13
            $configuration = $this->options->filtered['configuration'];
90 13
        } else {
91 7
            $configuration = new Configuration('');
92
        }
93
94 20
        $excludedGroups = array_merge($this->options->excludeGroups, $configuration->getExcludedGroups());
95 13
        $this->options->excludeGroups = $excludedGroups;
96
97 13
        if ($path) {
98 3
            $this->loadPath($path);
99 13
        } elseif (isset($this->options->testsuite) && $this->options->testsuite) {
100 7
            foreach ($configuration->getSuiteByName($this->options->testsuite) as $suite) {
101 7
                foreach ($suite as $suitePath) {
102 7
                    $this->loadPath($suitePath);
103 7
                }
104 7
            }
105 10
        } elseif ($suites = $configuration->getSuites()) {
106 2
            foreach ($suites as $suite) {
107 2
                foreach ($suite as $suitePath) {
108 3
                    $this->loadPath($suitePath);
109 2
                }
110 2
            }
111 9
        }
112
113 12
        if (!$this->files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
114
            throw new \RuntimeException("No path or configuration provided (tests must end with Test.php)");
115
        }
116
117 12
        $this->files = array_unique($this->files); // remove duplicates
118
119 12
        $this->initSuites();
120 12
    }
121
122
    /**
123
     * Loads suites based on a specific path.
124
     * A valid path can be a directory or file
125
     *
126
     * @param $path
127
     * @throws \InvalidArgumentException
128
     */
129 12
    private function loadPath($path)
130
    {
131 12
        $path = $path ? : $this->options->path;
132 12
        if ($path instanceof SuitePath) {
133 9
            $pattern = $path->getPattern();
134 9
            $path = $path->getPath();
135 9
        } else {
136 3
            $pattern = self::TEST_PATTERN;
137
        }
138 12
        if (!file_exists($path)) {
139
            throw new \InvalidArgumentException("$path is not a valid directory or file");
140
        }
141 12
        if (is_dir($path)) {
142 9
            $this->loadDir($path, $pattern);
143 12
        } elseif (file_exists($path)) {
144 6
            $this->loadFile($path);
145 6
        }
146 12
    }
147
148
    /**
149
     * Loads suites from a directory
150
     *
151
     * @param string $path
152
     * @param string $pattern
153
     */
154 9
    private function loadDir($path, $pattern = self::TEST_PATTERN)
155
    {
156 9
        $files = scandir($path);
157 9
        foreach ($files as $file) {
158 9
            $this->tryLoadTests($path . DIRECTORY_SEPARATOR . $file, $pattern);
159 9
        }
160 9
    }
161
162
    /**
163
     * Load a single suite file
164
     *
165
     * @param $path
166
     */
167 6
    private function loadFile($path)
168
    {
169 6
        $this->tryLoadTests($path, self::FILE_PATTERN);
170 6
    }
171
172
    /**
173
     * Attempts to load suites from a path.
174
     *
175
     * @param string $path
176
     * @param string $pattern regular expression for matching file names
177
     */
178 12
    private function tryLoadTests($path, $pattern = self::TEST_PATTERN)
179
    {
180 12
        if (preg_match($pattern, $path)) {
181 12
            $this->files[] = $path;
182 12
        }
183
184 12
        if (!preg_match(self::$dotPattern, $path) && is_dir($path)) {
185 6
            $this->loadDir($path, $pattern);
186 6
        }
187 12
    }
188
189
    /**
190
     * Called after all files are loaded. Parses loaded files into
191
     * ExecutableTest objects - either Suite or TestMethod
192
     */
193 12
    private function initSuites()
194
    {
195 12
        foreach ($this->files as $path) {
196
            try {
197 12
                $parser = new Parser($path);
198 12
                if ($class = $parser->getClass()) {
199 12
                    $suite = $this->createSuite($path, $class);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $suite is correct as $this->createSuite($path, $class) (which targets ParaTest\Runners\PHPUnit...teLoader::createSuite()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
200 12
                    if ($suite) {
201 12
                        $this->loadedSuites[$path] = $suite;
202 12
                    }
203 12
                }
204 12
            } catch (NoClassInFileException $e) {
205
                continue;
206
            }
207 12
        }
208 12
    }
209
210 12
    private function executableTests($path, $class)
211
    {
212 12
        $executableTests = array();
213 12
        $methodBatches = $this->getMethodBatches($class);
214 12
        foreach ($methodBatches as $methodBatch) {
215 12
            $executableTest = new TestMethod($path, $methodBatch);
216 12
            $executableTests[] = $executableTest;
217 12
        }
218 12
        return $executableTests;
219
    }
220
221
    /**
222
     * Get method batches.
223
     *
224
     * Identify method dependencies, and group dependents and dependees on a single methodBatch.
225
     * Use max batch size to fill batches.
226
     *
227
     * @param  ParsedClass $class
228
     * @return array of MethodBatches. Each MethodBatch has an array of method names
229
     */
230 12
    private function getMethodBatches(ParsedClass $class)
231
    {
232 12
        $classGroups = $this->classGroups($class);
233 12
        $classMethods = $class->getMethods($this->options ? $this->options->annotations : array());
234 12
        $maxBatchSize = $this->options && $this->options->functional ? $this->options->maxBatchSize : 0;
235 12
        $batches = array();
236 12
        foreach ($classMethods as $method) {
237 12
            $tests = $this->getMethodTests($class, $classGroups, $method, $maxBatchSize != 0);
238
239
            // if filter passed to paratest then method tests can be blank if not match to filter
240 12
            if (!$tests) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tests of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
241
                continue;
242
            }
243
244 12
            if (($dependsOn = $this->methodDependency($method)) != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $dependsOn = $this->methodDependency($method) of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
245
                $this->addDependentTestsToBatchSet($batches, $dependsOn, $tests);
246
            } else {
247 12
                $this->addTestsToBatchSet($batches, $tests, $maxBatchSize);
248
            }
249 12
        }
250
251 12
        return $batches;
252
    }
253
254
    private function addDependentTestsToBatchSet(&$batches, $dependsOn, $tests)
255
    {
256
        foreach ($batches as $key => $batch) {
257
            foreach ($batch as $methodName) {
258
                if ($dependsOn === $methodName) {
259
                    $batches[$key] = array_merge($batches[$key], $tests);
260
                    continue;
261
                }
262
            }
263
        }
264
    }
265
266 12
    private function addTestsToBatchSet(&$batches, $tests, $maxBatchSize)
267
    {
268 12
        foreach ($tests as $test) {
269 12
            $lastIndex = count($batches) - 1;
270
            if ($lastIndex != -1
271 12
                && count($batches[$lastIndex]) < $maxBatchSize
272 12
            ) {
273
                $batches[$lastIndex][] = $test;
274
            } else {
275 12
                $batches[] = array($test);
276
            }
277 12
        }
278 12
    }
279
280
    /**
281
     * Get method all available tests.
282
     *
283
     * With empty filter this method returns single test if doesnt' have data provider or
284
     * data provider is not used and return all test if has data provider and data provider is used.
285
     *
286
     * @param  ParsedClass  $class            Parsed class.
287
     * @param  array        $classGroups      Groups on the class.
288
     * @param  ParsedObject $method           Parsed method.
289
     * @param  bool         $useDataProvider  Try to use data provider or not.
290
     * @return string[]     Array of test names.
291
     */
292 12
    private function getMethodTests(ParsedClass $class, array $classGroups, ParsedObject $method, $useDataProvider = false)
293
    {
294 12
        $result = array();
295
296 12
        $groups = array_merge($classGroups, $this->methodGroups($method));
297
298 12
        $dataProvider = $this->methodDataProvider($method);
299 12
        if ($useDataProvider && isset($dataProvider)) {
300
            $testFullClassName = "\\" . $class->getName();
301
            $testClass = new $testFullClassName();
302
            $result = array();
303
            $datasetKeys = array_keys($testClass->$dataProvider());
304
            foreach ($datasetKeys as $key) {
305
                $test = sprintf(
306
                    "%s with data set %s",
307
                    $method->getName(),
308
                    is_int($key) ? "#" . $key : "\"" . $key . "\""
309
                );
310
                if ($this->testMatchOptions($class->getName(), $test, $groups)) {
311
                    $result[] = $test;
312
                }
313
            }
314 12
        } elseif ($this->testMatchOptions($class->getName(), $method->getName(), $groups)) {
315 12
            $result = array($method->getName());
316 12
        }
317
318 12
        return $result;
319
    }
320
321 12
    private function testMatchGroupOptions($groups)
322
    {
323 12
        if (empty($groups)) {
324 11
            return true;
325
        }
326
327 12
        if (!empty($this->options->groups)
328 12
            && !array_intersect($groups, $this->options->groups)
329 12
        ) {
330
            return false;
331
        }
332
333 12
        if (!empty($this->options->excludeGroups)
334 12
            && array_intersect($groups, $this->options->excludeGroups)
335 12
        ) {
336
            return false;
337
        }
338
339 12
        return true;
340
    }
341
342 12
    private function testMatchFilterOptions($className, $name, $group)
0 ignored issues
show
Unused Code introduced by
The parameter $group is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
343
    {
344 12
        if (empty($this->options->filter)) {
345 12
            return true;
346
        }
347
348
        $re = substr($this->options->filter, 0, 1) == "/"
349
            ? $this->options->filter
350
            : "/" . $this->options->filter . "/";
351
        $fullName = $className . "::" . $name;
352
        $result = preg_match($re, $fullName);
353
354
        return $result;
355
    }
356
357 12
    private function testMatchOptions($className, $name, $group)
358
    {
359 12
        $result = $this->testMatchGroupOptions($group)
360 12
                && $this->testMatchFilterOptions($className, $name, $group);
361
362 12
        return $result;
363
    }
364
365 12
    private function methodDataProvider($method)
366
    {
367 12
        if (preg_match("/@\bdataProvider\b \b(.*)\b/", $method->getDocBlock(), $matches)) {
368
            return $matches[1];
369
        }
370 12
        return null;
371
    }
372
373 12
    private function methodDependency($method)
374
    {
375 12
        if (preg_match("/@\bdepends\b \b(.*)\b/", $method->getDocBlock(), $matches)) {
376
            return $matches[1];
377
        }
378 12
        return null;
379
    }
380
381 12
    private function classGroups(ParsedClass $class)
382
    {
383 12
        return $this->docBlockGroups($class->getDocBlock());
384
    }
385
386 12
    private function methodGroups(ParsedObject $method)
387
    {
388 12
        return $this->docBlockGroups($method->getDocBlock());
389
    }
390
391 12
    private function docBlockGroups($docBlock)
392
    {
393 12
        if (preg_match_all("/@\bgroup\b \b(.*)\b/", $docBlock, $matches)) {
394 12
            return $matches[1];
395
        }
396 12
        return array();
397
    }
398
399 12
    private function createSuite($path, ParsedClass $class)
400
    {
401 12
        $executableTests = $this->executableTests(
402 12
          $path,
403
          $class
404 12
        );
405
406 12
        if (count($executableTests) > 0) {
407 12
            return new Suite($path, $executableTests, $class->getName());
408
        }
409
410
        return null;
411
    }
412
}
413