Completed
Push — master ( e546fc...9f6d8c )
by Julian
11s
created

src/Runners/PHPUnit/SuiteLoader.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
     * @var Options
27
     */
28
    public $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 3
    public function getTestMethods()
57
    {
58 3
        $methods = [];
59 3
        foreach ($this->loadedSuites as $suite) {
60 2
            $methods = array_merge($methods, $suite->getFunctions());
61
        }
62
63 3
        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
        } 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 14
        } elseif (isset($this->options->testsuite) && $this->options->testsuite) {
85 10
            foreach ($configuration->getSuiteByName($this->options->testsuite) as $suite) {
86 10
                foreach ($suite as $suitePath) {
87 10
                    $testFileLoader = new TestFileLoader($this->options);
88 10
                    $this->files = array_merge($this->files, $testFileLoader->loadSuitePath($suitePath));
89
                }
90
            }
91 4
        } elseif ($suites = $configuration->getSuites()) {
92 2
            foreach ($suites as $suite) {
93 2
                foreach ($suite as $suitePath) {
94 2
                    $testFileLoader = new TestFileLoader($this->options);
95 2
                    $this->files = array_merge($this->files, $testFileLoader->loadSuitePath($suitePath));
96
                }
97
            }
98
        }
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
                }
123 1
            } catch (NoClassInFileException $e) {
124 20
                continue;
125
            }
126
        }
127 20
    }
128
129 19
    private function executableTests($path, $class)
130
    {
131 19
        $executableTests = [];
132 19
        $methodBatches = $this->getMethodBatches($class);
133 19
        foreach ($methodBatches as $methodBatch) {
134 19
            $executableTest = new TestMethod($path, $methodBatch);
135 19
            $executableTests[] = $executableTest;
136
        }
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 : []);
152 19
        $maxBatchSize = $this->options && $this->options->functional ? $this->options->maxBatchSize : 0;
153 19
        $batches = [];
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) {
0 ignored issues
show
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...
162 1
                $this->addDependentTestsToBatchSet($batches, $dependsOn, $tests);
163
            } else {
164 19
                $this->addTestsToBatchSet($batches, $tests, $maxBatchSize);
165
            }
166
        }
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
            }
179
        }
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 19
            if ($lastIndex != -1
187 19
                && count($batches[$lastIndex]) < $maxBatchSize
188
            ) {
189
                $batches[$lastIndex][] = $test;
190
            } else {
191 19
                $batches[] = [$test];
192
            }
193
        }
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 = [];
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 = [];
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 = [$method->getName()];
231
        }
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
        ) {
245
            return false;
246
        }
247
248 15
        if (!empty($this->options->excludeGroups)
249 15
            && array_intersect($groups, $this->options->excludeGroups)
250
        ) {
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 [];
302
    }
303
304 19
    private function createSuite($path, ParsedClass $class)
305
    {
306 19
        return new Suite(
307
            $path,
308 19
            $this->executableTests(
309
                $path,
310
                $class
311
            ),
312 19
            $class->getName()
313
        );
314
    }
315
}
316