-
Notifications
You must be signed in to change notification settings - Fork 58
/
TestsSplitter.php
259 lines (217 loc) · 7.95 KB
/
TestsSplitter.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
<?php
declare(strict_types=1);
namespace Codeception\Task\Splitter;
use Codeception\Configuration;
use Codeception\Task\Filter\DefaultFilter;
use Codeception\Task\Filter\Filter;
use ReflectionClass;
use Robo\Exception\TaskException;
use Robo\Task\BaseTask;
use RuntimeException;
abstract class TestsSplitter extends BaseTask
{
protected int $numGroups;
protected string $projectRoot = '.';
/** @var string[]|string */
protected $testsFrom = 'tests';
protected string $saveTo = 'tests/_data/paracept_';
protected string $excludePath = 'vendor';
/** @var Filter[] $filter */
protected array $filter = [];
/**
* TestsSplitter constructor.
*
* @param int $groups number of groups to use
*/
public function __construct(int $groups)
{
$this->numGroups = $groups;
$this->filter[] = new DefaultFilter();
}
public function addFilter(Filter $filter): TestsSplitter
{
if (!in_array($filter, $this->filter, true)) {
$this->filter[] = $filter;
}
return $this;
}
public function getProjectRoot(): string
{
return realpath($this->projectRoot);
}
public function projectRoot(string $path): TestsSplitter
{
$this->projectRoot = $path;
return $this;
}
/**
* @param string[]|string $path - a single path or array of paths
* @return $this|TestsSplitter
*/
public function testsFrom($path): TestsSplitter
{
$this->testsFrom = $path;
return $this;
}
public function groupsTo(string $pattern): TestsSplitter
{
$this->saveTo = $pattern;
return $this;
}
public function excludePath(string $path): TestsSplitter
{
$this->excludePath = $path;
return $this;
}
/**
* @param mixed $item
*/
protected function resolveDependencies(
$item,
array $items,
array $resolved,
array $unresolved
): array {
$unresolved[] = $item;
foreach ($items[$item] as $dep) {
if (!in_array($dep, $resolved, true)) {
if (!in_array($dep, $unresolved, true)) {
$unresolved[] = $dep;
[$resolved, $unresolved] =
$this->resolveDependencies($dep, $items, $resolved, $unresolved);
} else {
throw new RuntimeException(sprintf('Circular dependency: %s -> %s', $item, $dep));
}
}
}
// Add $item to $resolved if it's not already there
if (!in_array($item, $resolved, true)) {
$resolved[] = $item;
}
// Remove all occurrences of $item in $unresolved
while (($index = array_search($item, $unresolved, true)) !== false) {
unset($unresolved[$index]);
}
return [$resolved, $unresolved];
}
/**
* Make sure that tests are in array are always with full path and name.
*/
protected function resolveDependenciesToFullNames(array $testsListWithDependencies): array
{
// make sure that dependencies are in array as full names
foreach ($testsListWithDependencies as $testName => $test) {
foreach ($test as $i => $dependency) {
if (is_a($dependency, '\PHPUnit\Framework\ExecutionOrderDependency')) {
// getTarget gives the classname::method
$dependency = $dependency->getTarget();
[$class, $method] = explode('::', $dependency);
$ref = new ReflectionClass($class);
$dependency = $ref->getFileName() . ':' . $method;
}
// sometimes it is written as class::method.
// for that reason we do trim in first case and replace from :: to one in second case
// just test name, that means that class name is the same, just different method name
if (strrpos($dependency, ':') === false) {
$testsListWithDependencies[$testName][$i] = trim(
substr($testName, 0, strrpos($testName, ':')),
':'
) . ':' . $dependency;
continue;
}
$dependency = str_replace('::', ':', $dependency);
// className:testName, that means we need to find proper test.
[$targetTestFileName, $targetTestMethodName] = explode(':', $dependency);
if (false === strrpos($targetTestFileName, '.php')) {
$targetTestFileName .= '.php';
}
// look for proper test in list of all tests. Test could be in different directory
// so we need to compare strings and if matched we just assign found test name
foreach (array_keys($testsListWithDependencies) as $arrayKey) {
if (
str_contains(
$arrayKey,
$targetTestFileName . ':' . $targetTestMethodName
)
) {
$testsListWithDependencies[$testName][$i] = $arrayKey;
continue 2;
}
}
throw new RuntimeException(
'Dependency target test ' . $dependency . ' not found.'
. 'Please make sure test exists and you are using full test name'
);
}
}
return $testsListWithDependencies;
}
/**
* Filter tests by the given filters, FIFO principal
*/
protected function filter(array $tests): array
{
foreach ($this->filter as $filter) {
$filter->setTests($tests);
$tests = $filter->filter();
}
return $tests;
}
/**
* Claims that the Codeception is loaded for Tasks which need it
*
* @throws TaskException
*/
protected function claimCodeceptionLoaded(): void
{
if (!$this->doCodeceptLoaderExists()) {
throw new TaskException(
$this,
'This task requires Codeception to be loaded. Please require autoload.php of Codeception'
);
}
// autoload PHPUnit files
\Codeception\PHPUnit\Init::init();
try {
// load Codeception config to set base directory
\Codeception\Configuration::config();
} catch (\Exception $e) {
$this->output()->writeln('Codeception config was not loaded, please load it manually');
}
}
protected function doCodeceptLoaderExists(): bool
{
return class_exists(\Codeception\Test\Loader::class);
}
/**
* Splitting array of files to the group files
*
* @param string[] $files - the relative path of the Testfile with or without test function
* @example $this->splitToGroupFiles(['tests/FooCest.php', 'tests/BarTest.php:testBarReturn']);
*/
protected function splitToGroupFiles(array $files): array
{
$i = 0;
$groups = [];
if (!Configuration::projectDir()) {
$this->output()->writeln("Codeception config was not loaded, paths to tests may not be set correctly.");
$this->output()->writeln("Execute \Codeception\Configuration::config() before this task");
}
$this->printTaskInfo('Processing ' . count($files) . ' files');
// splitting tests by groups
/** @var string $file */
foreach ($files as $file) {
$groups[($i % $this->numGroups) + 1][] = $file;
++$i;
}
$filenames = [];
// saving group files
foreach ($groups as $i => $tests) {
$filename = $this->saveTo . $i;
$this->printTaskInfo("Writing {$filename}");
file_put_contents($filename, implode("\n", $tests));
$filenames[] = $filename;
}
return $filenames;
}
}