Completed
Branch master (cb3538)
by Julian
02:11
created

SuiteLoader::getTestMethods()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
crap 2
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 = [];
17
18
    /**
19
     * The collection of parsed test classes
20
     *
21
     * @var array
22
     */
23
    protected $loadedSuites = [];
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 3
    public function getTestMethods()
52
    {
53 3
        $methods = [];
54 3
        foreach ($this->loadedSuites as $suite) {
55 2
            $methods = array_merge($methods, $suite->getFunctions());
56
        }
57
58 3
        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
        } 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 13
        } 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
                }
85
            }
86 4
        } 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
                }
92
            }
93
        }
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 $path) {
113
            try {
114 20
                $parser = new Parser($path);
115 19
                if ($class = $parser->getClass()) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $class is correct as $parser->getClass() (which targets ParaTest\Parser\Parser::getClass()) 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...
116 19
                    $this->loadedSuites[$path] = $this->createSuite($path, $class);
117
                }
118 1
            } catch (NoClassInFileException $e) {
119 1
                continue;
120
            }
121
        }
122 20
    }
123
124 19
    private function executableTests($path, $class)
125
    {
126 19
        $executableTests = [];
127 19
        $methodBatches = $this->getMethodBatches($class);
128 19
        foreach ($methodBatches as $methodBatch) {
129 19
            $executableTest = new TestMethod($path, $methodBatch);
130 19
            $executableTests[] = $executableTest;
131
        }
132 19
        return $executableTests;
133
    }
134
135
    /**
136
     * Get method batches.
137
     *
138
     * Identify method dependencies, and group dependents and dependees on a single methodBatch.
139
     * Use max batch size to fill batches.
140
     *
141
     * @param  ParsedClass $class
142
     * @return array of MethodBatches. Each MethodBatch has an array of method names
143
     */
144 19
    private function getMethodBatches($class)
145
    {
146 19
        $classMethods = $class->getMethods($this->options ? $this->options->annotations : []);
147 19
        $maxBatchSize = $this->options && $this->options->functional ? $this->options->maxBatchSize : 0;
148 19
        $batches = [];
149 19
        foreach ($classMethods as $method) {
150 19
            $tests = $this->getMethodTests($class, $method, $maxBatchSize != 0);
151
            // if filter passed to paratest then method tests can be blank if not match to filter
152 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...
153
                continue;
154
            }
155
156 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...
157 1
                $this->addDependentTestsToBatchSet($batches, $dependsOn, $tests);
158
            } else {
159 19
                $this->addTestsToBatchSet($batches, $tests, $maxBatchSize);
160
            }
161
        }
162 19
        return $batches;
163
    }
164
165 1
    private function addDependentTestsToBatchSet(&$batches, $dependsOn, $tests)
166
    {
167 1
        foreach ($batches as $key => $batch) {
168 1
            foreach ($batch as $methodName) {
169 1
                if ($dependsOn === $methodName) {
170 1
                    $batches[$key] = array_merge($batches[$key], $tests);
171 1
                    continue;
172
                }
173
            }
174
        }
175 1
    }
176
177 19
    private function addTestsToBatchSet(&$batches, $tests, $maxBatchSize)
178
    {
179 19
        foreach ($tests as $test) {
180 19
            $lastIndex = count($batches) - 1;
181 19
            if ($lastIndex != -1
182 16
                && count($batches[$lastIndex]) < $maxBatchSize
183
            ) {
184
                $batches[$lastIndex][] = $test;
185
            } else {
186 19
                $batches[] = [$test];
187
            }
188
        }
189 19
    }
190
191
    /**
192
     * Get method all available tests.
193
     *
194
     * With empty filter this method returns single test if doesnt' have data provider or
195
     * data provider is not used and return all test if has data provider and data provider is used.
196
     *
197
     * @param  ParsedClass  $class            Parsed class.
198
     * @param  ParsedObject $method           Parsed method.
199
     * @param  bool         $useDataProvider  Try to use data provider or not.
200
     * @return string[]     Array of test names.
201
     */
202 19
    private function getMethodTests($class, $method, $useDataProvider = false)
203
    {
204 19
        $result = [];
205
206 19
        $groups = $this->methodGroups($method);
207
208 19
        $dataProvider = $this->methodDataProvider($method);
209 19
        if ($useDataProvider && isset($dataProvider)) {
210
            $testFullClassName = "\\" . $class->getName();
211
            $testClass = new $testFullClassName();
212
            $result = [];
213
            $datasetKeys = array_keys($testClass->$dataProvider());
214
            foreach ($datasetKeys as $key) {
215
                $test = sprintf(
216
                    "%s with data set %s",
217
                    $method->getName(),
218
                    is_int($key) ? "#" . $key : "\"" . $key . "\""
219
                );
220
                if ($this->testMatchOptions($class->getName(), $test, $groups)) {
221
                    $result[] = $test;
222
                }
223
            }
224 19
        } elseif ($this->testMatchOptions($class->getName(), $method->getName(), $groups)) {
225 19
            $result = [$method->getName()];
226
        }
227
228 19
        return $result;
229
    }
230
231 19
    private function testMatchGroupOptions($groups)
232
    {
233 19
        if (empty($groups)) {
234 17
            return true;
235
        }
236
237 15
        if (!empty($this->options->groups)
238 1
            && !array_intersect($groups, $this->options->groups)
239
        ) {
240
            return false;
241
        }
242
243 15
        if (!empty($this->options->excludeGroups)
244
            && array_intersect($groups, $this->options->excludeGroups)
245
        ) {
246
            return false;
247
        }
248
249 15
        return true;
250
    }
251
252 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...
253
    {
254 19
        if (empty($this->options->filter)) {
255 19
            return true;
256
        }
257
258
        $re = substr($this->options->filter, 0, 1) == "/"
259
            ? $this->options->filter
260
            : "/" . $this->options->filter . "/";
261
        $fullName = $className . "::" . $name;
262
        $result = preg_match($re, $fullName);
263
264
        return $result;
265
    }
266
267 19
    private function testMatchOptions($className, $name, $group)
268
    {
269 19
        $result = $this->testMatchGroupOptions($group)
270 19
                && $this->testMatchFilterOptions($className, $name, $group);
271
272 19
        return $result;
273
    }
274
275 19
    private function methodDataProvider($method)
276
    {
277 19
        if (preg_match("/@\bdataProvider\b \b(.*)\b/", $method->getDocBlock(), $matches)) {
278
            return $matches[1];
279
        }
280 19
        return null;
281
    }
282
283 19
    private function methodDependency($method)
284
    {
285 19
        if (preg_match("/@\bdepends\b \b(.*)\b/", $method->getDocBlock(), $matches)) {
286 1
            return $matches[1];
287
        }
288 19
        return null;
289
    }
290
291 19
    private function methodGroups($method)
292
    {
293 19
        if (preg_match_all("/@\bgroup\b \b(.*)\b/", $method->getDocBlock(), $matches)) {
294 15
            return $matches[1];
295
        }
296 17
        return [];
297
    }
298
299 19
    private function createSuite($path, ParsedClass $class)
300
    {
301 19
        return new Suite(
302
            $path,
303 19
            $this->executableTests(
304 19
                $path,
305 19
                $class
306
            ),
307 19
            $class->getName()
308
        );
309
    }
310
}
311