Skip to content

Instantly share code, notes, and snippets.

@adamgoucher
Created February 11, 2013 00:47
Show Gist options
  • Save adamgoucher/4751742 to your computer and use it in GitHub Desktop.
Save adamgoucher/4751742 to your computer and use it in GitHub Desktop.
a parallel runner for phpunit. worked well enough, but isn't os portable
<?php
$max_sauce_concurrency = 16;
set_include_path(get_include_path() . PATH_SEPARATOR . './shared');
set_include_path(get_include_path() . PATH_SEPARATOR . './pages');
set_include_path(get_include_path() . PATH_SEPARATOR . './parallel_runner');
require_once 'PHPUnit/Autoload.php';
require_once 'parallel_runner/util.php';
require_once 'shared/reporting.php';
$want_groups = array();
$dont_want_groups = array();
$strategy = 1;
$longopts = array(
"group::", // group (want)
"exclude-group::", // exclude (don't want)
"parallel::"
);
$options = getopt("", $longopts);
if (array_key_exists("group", $options)) {
$want_groups = explode(",", $options["group"]);
}
// print_r($want_groups);
if (array_key_exists("exclude-group", $options)) {
$dont_want_groups = explode(",", $options["exclude-group"]);
}
// print_r($dont_want_groups);
if (array_key_exists("parallel", $options)) {
$strategy = $options["parallel"];
}
$desired_test_methods = array();
foreach (glob("scripts/*.php") as $filename) {
$old_classes = array_keys(get_declared_classes());
include_once dirname(__FILE__) . '/' . $filename;
$new_classes = get_declared_classes();
$new_class_names = array_diff(
array_keys($new_classes), $old_classes
);
foreach ($new_class_names as $class_name) {
$reflected_class = new ReflectionClass($new_classes[$class_name]);
// can't be an abstract class
if (!$reflected_class->isAbstract()) {
// has to inherit from PHPUnit_Framework_TestCase; but instanceof wasn't working
// so check that it implements the interface PHPUnit_Framework_Test since thats
// what PHPUnit_Framework_TestCase does anyways
if ($reflected_class->implementsInterface('PHPUnit_Framework_Test')) {
// print_r($new_classes[$class_name]);
foreach ($reflected_class->getMethods() as $method) {
if (preg_match('/@test/', $method->getDocComment(), $matches)) {
$got_annotations = Parallel_Runner_Util::parseAnnotations($method->getDocComment());
if (array_key_exists("group", $got_annotations)) {
foreach (array_values($got_annotations["group"]) as $got_group) {
$string_repr = $filename . '::' . $new_classes[$class_name] . '::' . $method->name;
if (in_array($got_group, $want_groups)) {
array_push($desired_test_methods, $string_repr);
}
if (in_array($got_group, $dont_want_groups)) {
$search_result = array_search($string_repr, $desired_test_methods);
if ($search_result) {
unset($desired_test_methods[$search_result]);
}
}
}
}
}
}
}
}
}
}
// print_r($desired_test_methods);
if ($strategy == "max" || count($desired_test_methods) == $strategy) {
if (count($desired_test_methods) > $max_sauce_concurrency) {
$how_many_chunks = $max_sauce_concurrency;
} else {
$how_many_chunks = 1;
}
$chunks = array_chunk($desired_test_methods, $how_many_chunks);
} else {
// theres gotta be a better algorithm here. if there are 11 $desired_test_methods and a strategy
// of 5 then you either get 4 chunks if using ceil() or 6 chunks if using round(). in theory cound
// see how below we are (less is better in this case) and move things around to be more distributed
// but thats not that simple to code up
if ($strategy > $max_sauce_concurrency) {
$strategy = $max_sauce_concurrency;
}
$how_many_chunks = ceil(count($desired_test_methods) / $strategy);
$chunks = array_chunk($desired_test_methods, $how_many_chunks);
}
$suite_result_files = array();
$current_jobs = array();
foreach ($chunks as $chunk) {
$config_filename = tempnam("tmp" . DIRECTORY_SEPARATOR . "configs", time().rand());
$results_filename = tempnam("tmp" . DIRECTORY_SEPARATOR . "results", time().rand());
array_push($suite_result_files, $results_filename);
$pid = pcntl_fork();
if ($pid == -1) {
return false;
}
else if ($pid) {
pcntl_wait($status);
} else {
$base_phpunit_xml = simplexml_load_file("phpunit.xml");
$testsuites = $base_phpunit_xml->addChild("testsuites");
$testsuite = $testsuites->addChild("testsuite");
$already = array();
$filters = "[";
foreach (array_values($chunk) as $test_info) {
$info = explode("::", $test_info);
$filters = $filters . $info[2] . "|";
if (!in_array($info[0], $already)) {
array_push($already, $info[0]);
$testsuite->addChild("file", $info[0]);
}
}
$filters = substr($filters, 0, -1) . "]";
$groups = $base_phpunit_xml->addChild("groups");
$include = $groups->addChild("include");
foreach (array_values($want_groups) as $group) {
$include->addChild("group", $group);
}
$exclude = $groups->addChild("exclude");
foreach (array_values($dont_want_groups) as $group) {
$exclude->addChild("group", $group);
}
$base_phpunit_xml->php->includePath = str_replace("./", __DIR__ . "/", $base_phpunit_xml->php->includePath);
$base_phpunit_xml->listeners->listener['file'] = str_replace("./", __DIR__ . "/", $base_phpunit_xml->listeners->listener['file']);
// print_r($base_phpunit_xml->asXML());
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($base_phpunit_xml->asXML());
// echo $dom->saveXML();
$dom->save($config_filename);
$cmd = 'phpunit --filter "' . $filters . '" --configuration ' . $config_filename . ' --log-junit ' . $results_filename;
// print_r($cmd);
exec($cmd);
break;
}
}
$junit_log_name = __DIR__ . "/logs/merged-results.xml";
$junit_log_handle = fopen($junit_log_name, "w");
// print_r($suite_result_files);
fwrite($junit_log_handle, merge_results($suite_result_files));
fclose($junit_log_handle);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment