Skip to content

Commit f7a03b8

Browse files
Add group-based split fallback if no runtime report exists.
This also cleans up error handling and improves logging. Closes #1.
1 parent 5c6b8f0 commit f7a03b8

File tree

3 files changed

+142
-50
lines changed

3 files changed

+142
-50
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
A [Codeception](https://codeception.com/) extension & [Robo](https://robo.li) task that records test runtimes and lets you split tests into equal runtime-based groups for parallel runs
44

5-
**Coming soon**
6-
75
# Usage
86
First, install this package:
97
```bash
@@ -67,7 +65,7 @@ require_once 'vendor/autoload.php';
6765
require_once 'vendor/codeception/codeception/autoload.php';
6866
```
6967

70-
Update these paths depending on where your `Robofile` lives, in relation to Composer's `vendor` directory.
68+
Update these paths depending on where your `Robofile` lives in relation to Composer's `vendor` directory.
7169

7270
This forces Codeception's autoloader to fire and redeclare the PHPUnit classes that it needs to function.
7371

src/Test.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace ChinthakaGodawita\CodeceptionTimekeeper;
5+
6+
use Codeception\Test\Descriptor as TestDescriptor;
7+
use Codeception\Test\Metadata;
8+
use Codeception\Test\Test as BaseTest;
9+
10+
class Test
11+
{
12+
13+
/**
14+
* @var BaseTest
15+
*/
16+
private $test;
17+
18+
/**
19+
* @var Metadata
20+
*/
21+
private $metadata;
22+
23+
/**
24+
* @var string
25+
*/
26+
private $path;
27+
28+
public function __construct(BaseTest $test)
29+
{
30+
$this->test = $test;
31+
}
32+
33+
/**
34+
* @return bool TRUE if this test has a `@skip` annotation on it.
35+
*/
36+
public function isSkipped(): bool
37+
{
38+
if ($this->metadata === null) {
39+
$this->metadata = $this->test->getMetadata();
40+
}
41+
return $this->metadata->getSkip() !== null;
42+
}
43+
44+
/**
45+
* Get relative path to a test file.
46+
*
47+
* @return string
48+
*/
49+
public function path(): string
50+
{
51+
if ($this->path === null) {
52+
$path = DIRECTORY_SEPARATOR . TestDescriptor::getTestFullName($this->test);
53+
// Robo updates PHP's current working directory to the location of the
54+
// Robofile.
55+
$currentDir = getcwd() . DIRECTORY_SEPARATOR;
56+
57+
if (strpos($path, $currentDir) === 0) {
58+
$path = substr($path, strlen($currentDir));
59+
}
60+
61+
$this->path = $path;
62+
}
63+
64+
return $this->path;
65+
}
66+
67+
}

src/TimeSplitterTask.php

Lines changed: 74 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33

44
namespace ChinthakaGodawita\CodeceptionTimekeeper;
55

6-
use Codeception\Test\Descriptor as TestDescriptor;
76
use Codeception\Test\Loader;
8-
use Codeception\TestInterface;
97
use Generator;
108
use PHPUnit\Framework\DataProviderTestSuite;
11-
use PHPUnit\Framework\SelfDescribing;
129
use Robo\Exception\TaskException;
1310
use Robo\Result;
1411
use Robo\Task\BaseTask;
@@ -33,7 +30,7 @@ class TimeSplitterTask extends BaseTask
3330
/**
3431
* @var string The filename pattern under which groups will be saved.
3532
*/
36-
private $groupOutputLoc = 'tests/_data/timekeeper/group_';
33+
private $groupOutputLoc = '_data/timekeeper/group_';
3734

3835
/**
3936
* @var string The directory that holds test files.
@@ -71,7 +68,16 @@ public function run(): Result
7168
$tests = $testLoader->getTests();
7269
$testCount = count($tests);
7370

74-
$this->printTaskInfo("Splitting {$testCount} into {$this->groupCount} groups of equal runtime...");
71+
if ($this->groupCount > $testCount) {
72+
return Result::error(
73+
$this,
74+
"Provided group count ({$this->groupCount}) is more than the number of tests ({$testCount})!"
75+
);
76+
}
77+
78+
$this->printTaskInfo(
79+
"Splitting {$testCount} tests into {$this->groupCount} groups of equal runtime..."
80+
);
7581

7682
$timeReport = null;
7783
try {
@@ -107,9 +113,14 @@ public function run(): Result
107113
}
108114
}
109115

110-
foreach ($groups as $idx => $tests) {
111-
$fileName = $this->groupOutputLoc . $idx;
112-
$this->printTaskInfo("Writing group {$idx} to: $fileName");
116+
$groupIdx = null;
117+
foreach ($groups as $groupIdx => $tests) {
118+
if (\count($tests) === 0) {
119+
break;
120+
}
121+
122+
$fileName = $this->groupOutputLoc . $groupIdx;
123+
$this->printTaskInfo("Writing group {$groupIdx} to: $fileName");
113124
$success = file_put_contents($fileName, implode("\n", $tests));
114125
if (!$success) {
115126
throw new TaskException(
@@ -119,27 +130,73 @@ public function run(): Result
119130
}
120131
}
121132

122-
return Result::success($this, "{$this->groupCount} test groups created");
133+
if ($groupIdx === null) {
134+
$this->printTaskWarning('No test groups were created');
135+
} else {
136+
$groupCount = $groupIdx + 1;
137+
$this->printTaskInfo("{$groupCount} test groups created");
138+
}
139+
140+
return Result::success($this);
123141
}
124142

143+
/**
144+
* Splits tests into equal groups, ignoring test runtimes.
145+
*
146+
* @param Generator|Test[] $tests
147+
*
148+
* @return array
149+
*/
125150
private function splitTestsByGroup(Generator $tests): array
126151
{
127-
// @TODO.
128-
return [];
152+
$groups = array_fill(0, $this->groupCount, []);
153+
$skippedTests = [];
154+
155+
$groupIdx = 0;
156+
foreach ($tests as $test) {
157+
$testPath = $test->path();
158+
159+
if ($test->isSkipped()) {
160+
$skippedTests[] = $testPath;
161+
} else {
162+
$groups[$groupIdx][] = $testPath;
163+
}
164+
165+
$groupIdx++;
166+
if ($groupIdx === $this->groupCount) {
167+
$groupIdx = 0;
168+
}
169+
}
170+
171+
// Add skipped tests onto the last group as they take relatively no time
172+
// to run.
173+
$maxGroupIdx = $this->groupCount - 1;
174+
if (count($skippedTests) > 0) {
175+
$groups[$maxGroupIdx] = array_merge($groups[$maxGroupIdx], $skippedTests);
176+
}
177+
178+
return $groups;
129179
}
130180

181+
/**
182+
* Splits tests into groups of _roughly_ equal runtimes.
183+
*
184+
* @param Generator|Test[] $tests
185+
* @param TimeReport $timeReport
186+
*
187+
* @return array
188+
*/
131189
private function splitTestsByRuntime(Generator $tests, TimeReport $timeReport): array
132190
{
133191
$skippedTests = [];
134192
$testsWithRuntime = [];
135193
$testsWithoutRuntime = [];
136194

137195
foreach ($tests as $test) {
138-
$testMeta = $test->getMetadata();
139-
$testPath = $this->getTestRelativePath($test);
196+
$testPath = $test->path();
140197
$runtime = $timeReport->getTime($testPath);
141198

142-
if ($testMeta->getSkip() !== null) {
199+
if ($test->isSkipped()) {
143200
$skippedTests[] = $testPath;
144201
} elseif ($runtime === null) {
145202
$testsWithoutRuntime[] = $testPath;
@@ -155,18 +212,10 @@ private function splitTestsByRuntime(Generator $tests, TimeReport $timeReport):
155212
$sums = array_fill(0, $this->groupCount, 0);
156213
$groups = array_fill(0, $this->groupCount, []);
157214

158-
$maxLoops = 3 * $this->groupCount;
159215
foreach ($testsWithRuntime as $testPath => $runtime) {
160216
$idx = 0;
161217
$loops = 0;
162218
while(true) {
163-
if ($loops > $maxLoops) {
164-
throw new TaskException(
165-
$this,
166-
"Max loop count ({$maxLoops}) reached."
167-
);
168-
}
169-
170219
$sum = $sums[$idx];
171220
$prevIdx = $idx - 1;
172221
if ($prevIdx < 0) {
@@ -176,13 +225,12 @@ private function splitTestsByRuntime(Generator $tests, TimeReport $timeReport):
176225
if ($nextIdx === $this->groupCount) {
177226
$nextIdx = 0;
178227
}
179-
if ($sum === 0 || ($sum < $sums[$prevIdx] && $sum < $sums[$nextIdx])) {
228+
if ($sum === 0 || (($sum < $sums[$prevIdx] && $sum < $sums[$nextIdx]))) {
180229
$sums[$idx] += $runtime;
181230
$groups[$idx][] = $testPath;
182231
break;
183232
}
184233
$idx++;
185-
$loops++;
186234
}
187235
}
188236

@@ -210,7 +258,7 @@ private function splitTestsByRuntime(Generator $tests, TimeReport $timeReport):
210258
/**
211259
* @param array $tests
212260
*
213-
* @return Generator|TestInterface[]
261+
* @return Generator|Test[]
214262
*/
215263
private function testIterator(array $tests): Generator
216264
{
@@ -219,29 +267,8 @@ private function testIterator(array $tests): Generator
219267
$test = current($test->tests());
220268
}
221269

222-
yield $test;
270+
yield new Test($test);
223271
}
224272
}
225273

226-
/**
227-
* Get the path to a particular test, relative to the Robofile.
228-
*
229-
* @param SelfDescribing $test
230-
*
231-
* @return string
232-
*/
233-
private function getTestRelativePath(SelfDescribing $test): string
234-
{
235-
$path = DIRECTORY_SEPARATOR . TestDescriptor::getTestFullName($test);
236-
// Robo updates PHP's current working directory to the location of the
237-
// Robofile.
238-
$currentDir = getcwd() . DIRECTORY_SEPARATOR;
239-
240-
if (strpos($path, $currentDir) === 0) {
241-
$path = substr($path, strlen($currentDir));
242-
}
243-
244-
return $path;
245-
}
246-
247274
}

0 commit comments

Comments
 (0)