Completed
Pull Request — master (#201)
by
unknown
03:56
created

SuiteLoader::initSuites()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6.0087

Importance

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