Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# PHPUnit Test Splitter

Allows you to split your PHPUnit tests by timings.

## Usage

Generate a timing file:

```bash
phpunit --cache-result --cache-result-file=.phpunit.result.cache
```

List your tests:

```bash
phpunit --list-tests-xml=tests.xml
```

This generates an XML file with a list of tests. You can add `--testsuite` to limit the tests to a specific suite.

Split the tests in 2 groups and get the first group (0):

```bash
phpunit-splitter 2 0 --tests-file=tests.xml --results-file=.phpunit.result.cache
```

Split the tests in 4 groups and get the third group (2):

```bash
phpunit-splitter 4 2 --tests-file=tests.xml --results-file=.phpunit.result.cache
```

Pass the results to PHPUnit:

```bash
./phpunit-splitter 2 0 --tests-file=tests/fixtures/tests.xml --results-file=tests/fixtures/.phpunit.result.cache | while IFS= read -r line; do
filepath=$(echo "$line" | awk '{print $NF}')
testname=$(echo "$line" | awk '{$NF=""; print $0}')
./vendor/bin/phpunit --filter="$testname" $filepath
done
```
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
"type": "project",
"require": {
"php": "^8.1",
"ext-simplexml": "*",
"phpunit/phpunit": "^9.6",
"previousnext/phpunit-finder": "^2.0"
"previousnext/phpunit-finder": "^2.0",
"symfony/console": "^6.3"
},
"autoload": {
"psr-4": {"PhpUnitSplitter\\": "src/"}
},
"autoload-dev": {
"psr-4": {"PhpUnitSplitter\\Tests\\": "tests/"}
"psr-4": {
"PhpUnitSplitter\\Tests\\": "tests/"
}
},
"config": {
"sort-packages": true
Expand Down
33 changes: 33 additions & 0 deletions phpunit-splitter
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env php
<?php
/**
* @file
* Console application for PHPUnit Splitter.
*/

$autoload = [
__DIR__ . '/vendor/autoload.php',
dirname(__DIR__, 1) . '/vendor/autoload.php',
dirname(__DIR__, 2) . '/vendor/autoload.php',
dirname(__DIR__, 1) . '/autoload.php',
dirname(__DIR__, 2) . '/autoload.php',
];
foreach ($autoload as $file) {
if (file_exists($file)) {
require $file;
break;
}
}

const APP_NAME = 'PHPUnit Splitter';
const VERSION = '1.x-dev';

use PhpUnitSplitter\SplitterCommand;
use Symfony\Component\Console\Application;


$application = new Application(APP_NAME, VERSION);
$command = new SplitterCommand('phpunit-splitter');
$application->add($command);
$application->setDefaultCommand($command->getName(), TRUE);
$application->run();
3 changes: 3 additions & 0 deletions scripts/generate-fixtures.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
./vendor/bin/phpunit --list-tests-xml=tests/fixtures/tests.xml tests/fixtures/Test
./vendor/bin/phpunit tests/fixtures/Test --cache-result --cache-result-file=tests/fixtures/.phpunit.result.cache
67 changes: 67 additions & 0 deletions src/SplitterCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace PhpUnitSplitter;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* A symfony command for splitting PHPUnit tests.
*/
class SplitterCommand extends Command {

/**
* {@inheritdoc}
*/
protected function configure(): void {
$this->addArgument('splits', InputArgument::OPTIONAL, "The number of splits", 1);
$this->addArgument('index', InputArgument::OPTIONAL, "The index of the current split", 0);
$this->addOption('tests-file', 't', InputOption::VALUE_REQUIRED, "The xml file listing all tests.", getcwd() . './tests.xml');
$this->addOption('results-file', 'f', InputOption::VALUE_REQUIRED, "The results cache file.", getcwd() . '/.phpunit.result.cache', );
$this->addOption('bootstrap-file', 'b', InputOption::VALUE_OPTIONAL, "The tests bootstrap file.", getcwd() . '/tests/bootstrap.php');
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$bootstrap = $input->getOption('bootstrap-file');
if (\file_exists($bootstrap)) {
include_once $bootstrap;
}
// @todo validation
$splits = (int) $input->getArgument('splits');
$index = (int) $input->getArgument('index');
$testsFile = $input->getOption('tests-file');
$resultsFile = $input->getOption('results-file');

$mapper = new TestMapper($testsFile, $resultsFile);
$map = $mapper->sortMap($mapper->getMap());

foreach ($this->split($map, $splits, $index) as $testName => $test) {
$output->writeln(\addslashes($testName) . ' ' . $test['path']);
}

return 0;
}

private function split(array $map, int $splits, int $index): array {
$result = [];
$keys = array_keys($map);
$values = array_values($map);

for ($i = $index; $i < count($map); $i++) {
if (($i - $index) % $splits === 0) {
$result[$keys[$i]] = $values[$i];
}
}

return $result;
}

}
60 changes: 60 additions & 0 deletions src/TestMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace PhpUnitSplitter;

use PHPUnit\Runner\DefaultTestResultCache;
use PHPUnit\Runner\TestResultCache;

/**
* Generates a map of test methods with their file name and execution time.
*/
final class TestMapper {

private \SimpleXMLElement|FALSE $testsXml;
private TestResultCache $resultCache;

public function __construct(string $testListFilePath, string $testResultFilePath) {
$this->testsXml = \simplexml_load_file($testListFilePath);
$this->resultCache = new DefaultTestResultCache($testResultFilePath);
}

public function getMap(): array {
$this->resultCache->load();
$map = [];
$classesXpath = $this->testsXml->xpath('//testCaseClass');
foreach ($classesXpath as $class) {
$className = (string) $class->attributes()['name'];
try {
$reflection = new \ReflectionClass($className);
}
catch (\ReflectionException $e) {
// Couldn't find the class.
continue;
}
$filename = $reflection->getFileName();
$testCases = $class->xpath('testCaseMethod');
foreach ($testCases as $testCase) {
$testName = $reflection->getShortName() . '::' . $testCase->attributes()['name'];
$dataSet = $testCase->attributes()['dataSet'] ?? NULL;
if ($dataSet !== NULL) {
$testName .= " with data set $dataSet";
}
$map[$testName] = [
'path' => $filename,
'time' => $this->resultCache->getTime($testName),
];
}
}
return $map;
}

public function sortMap(array $map): array {
uasort($map, function ($a, $b) {
return $a['time'] <=> $b['time'];
});
return $map;
}

}
55 changes: 55 additions & 0 deletions tests/PhpUnitSplitterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace PhpUnitSplitter\Tests;

use PHPUnit\Framework\TestCase;
use PhpUnitSplitter\TestMapper;

class PhpUnitSplitterTest extends TestCase {

public function testSplitter(): void {
$fixtures = dirname(__DIR__) . '/tests/fixtures';
$mapper = new TestMapper("$fixtures/tests.xml", "$fixtures/.phpunit.result.cache");
$map = $mapper->getMap();

$this->assertSame([
'PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testOne',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testTwo',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testThree',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testFour',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testFive',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set "one"',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set "two"',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set "three"',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set "four"',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set "five"',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testOne',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testTwo',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testThree',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testFour',
'PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testFive',
], array_keys($map));

$sorted = $mapper->sortMap($map);
$this->assertSame([
'PhpUnitSplitter\Tests\fixtures\Test\FastTestsTest::testOne',
'PhpUnitSplitter\Tests\fixtures\Test\FastTestsTest::testTwo',
'PhpUnitSplitter\Tests\fixtures\Test\FastTestsTest::testThree',
'PhpUnitSplitter\Tests\fixtures\Test\FastTestsTest::testFour',
'PhpUnitSplitter\Tests\fixtures\Test\FastTestsTest::testFive',
'PhpUnitSplitter\Tests\fixtures\Test\SlowTestsTest::testOne',
'PhpUnitSplitter\Tests\fixtures\Test\ProviderTest::testProvider with data set "one"',
'PhpUnitSplitter\Tests\fixtures\Test\SlowTestsTest::testTwo',
'PhpUnitSplitter\Tests\fixtures\Test\ProviderTest::testProvider with data set "three"',
'PhpUnitSplitter\Tests\fixtures\Test\SlowTestsTest::testThree',
'PhpUnitSplitter\Tests\fixtures\Test\ProviderTest::testProvider with data set "four"',
'PhpUnitSplitter\Tests\fixtures\Test\SlowTestsTest::testFour',
'PhpUnitSplitter\Tests\fixtures\Test\ProviderTest::testProvider with data set "two"',
'PhpUnitSplitter\Tests\fixtures\Test\SlowTestsTest::testFive',
'PhpUnitSplitter\Tests\fixtures\Test\ProviderTest::testProvider with data set "five"',
], array_keys($sorted));

}
}
1 change: 1 addition & 0 deletions tests/fixtures/.phpunit.result.cache
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":1,"defects":[],"times":{"PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testOne":0.012,"PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testTwo":0.02,"PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testThree":0.03,"PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testFour":0.04,"PhpUnitSplitter\\Tests\\fixtures\\Test\\FastTestsTest::testFive":0.05,"PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testOne":0.1,"PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testTwo":0.2,"PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testThree":0.3,"PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testFour":0.4,"PhpUnitSplitter\\Tests\\fixtures\\Test\\SlowTestsTest::testFive":0.5,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set #0":1,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set #1":0.111,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set #2":0.445,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set #3":0.222,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set #4":0.334,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set #5":0.667,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set \"one\"":0.111,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set \"two\"":0.445,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set \"three\"":0.222,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set \"four\"":0.334,"PhpUnitSplitter\\Tests\\fixtures\\Test\\ProviderTest::testProvider with data set \"five\"":0.667}}
36 changes: 36 additions & 0 deletions tests/fixtures/Test/FastTestsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace PhpUnitSplitter\Tests\fixtures\Test;

use PHPUnit\Framework\TestCase;

class FastTestsTest extends TestCase {

function testOne(): void {
usleep(10000);
$this->assertTrue(TRUE);
}

function testTwo(): void {
usleep(20000);
$this->assertTrue(TRUE);
}

function testThree(): void {
usleep(30000);
$this->assertTrue(TRUE);
}

function testFour(): void {
usleep(40000);
$this->assertTrue(TRUE);
}

function testFive(): void {
usleep(50000);
$this->assertTrue(TRUE);
}

}
30 changes: 30 additions & 0 deletions tests/fixtures/Test/ProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace PhpUnitSplitter\Tests\fixtures\Test;

use PHPUnit\Framework\TestCase;

class ProviderTest extends TestCase {

/**
* @dataProvider provider
*/
function testProvider(int $sleep): void {
usleep($sleep);
$this->assertTrue(TRUE);
}

public function provider(): array {
return [
'one' => [111111],
'two' => [444444],
'three' => [222222],
'four' => [333333],
'five' => [666666],
];
}


}
36 changes: 36 additions & 0 deletions tests/fixtures/Test/SlowTestsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace PhpUnitSplitter\Tests\fixtures\Test;

use PHPUnit\Framework\TestCase;

class SlowTestsTest extends TestCase {

function testOne(): void {
usleep(100000);
$this->assertTrue(TRUE);
}

function testTwo(): void {
usleep(200000);
$this->assertTrue(TRUE);
}

function testThree(): void {
usleep(300000);
$this->assertTrue(TRUE);
}

function testFour(): void {
usleep(400000);
$this->assertTrue(TRUE);
}

function testFive(): void {
usleep(500000);
$this->assertTrue(TRUE);
}

}
Loading