2012-05-15 15:12:10 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* PHPUnit wrapper
|
|
|
|
*
|
2012-11-21 00:37:31 +01:00
|
|
|
* To use, set unit.engine in .arcconfig, or use --engine flag
|
2012-05-15 15:12:10 +02:00
|
|
|
* with arc unit. Currently supports only class & test files
|
|
|
|
* (no directory support).
|
|
|
|
* To use custom phpunit configuration, set phpunit_config in
|
|
|
|
* .arcconfig (e.g. app/phpunit.xml.dist).
|
|
|
|
*
|
|
|
|
* @group unitrun
|
|
|
|
*/
|
|
|
|
final class PhpunitTestEngine extends ArcanistBaseUnitTestEngine {
|
|
|
|
|
|
|
|
private $configFile;
|
2013-08-26 18:59:51 +02:00
|
|
|
private $phpunitBinary = 'phpunit';
|
2012-05-15 15:12:10 +02:00
|
|
|
private $affectedTests;
|
|
|
|
private $projectRoot;
|
|
|
|
|
|
|
|
public function run() {
|
|
|
|
|
|
|
|
$this->projectRoot = $this->getWorkingCopy()->getProjectRoot();
|
|
|
|
$this->affectedTests = array();
|
|
|
|
foreach ($this->getPaths() as $path) {
|
|
|
|
|
2012-07-20 00:00:14 +02:00
|
|
|
$path = Filesystem::resolvePath($path, $this->projectRoot);
|
2012-05-15 15:12:10 +02:00
|
|
|
|
|
|
|
// TODO: add support for directories
|
|
|
|
// Users can call phpunit on the directory themselves
|
|
|
|
if (is_dir($path)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not sure if it would make sense to go further if
|
|
|
|
// it is not a .php file
|
|
|
|
if (substr($path, -4) != '.php') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (substr($path, -8) == 'Test.php') {
|
|
|
|
// Looks like a valid test file name.
|
|
|
|
$this->affectedTests[$path] = $path;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($test = $this->findTestFile($path)) {
|
|
|
|
$this->affectedTests[$path] = $test;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (empty($this->affectedTests)) {
|
|
|
|
throw new ArcanistNoEffectException('No tests to run.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->prepareConfigFile();
|
|
|
|
$futures = array();
|
|
|
|
$tmpfiles = array();
|
|
|
|
foreach ($this->affectedTests as $class_path => $test_path) {
|
2013-06-20 16:35:18 +02:00
|
|
|
if(!Filesystem::pathExists($test_path)) {
|
|
|
|
continue;
|
|
|
|
}
|
2012-05-15 15:12:10 +02:00
|
|
|
$json_tmp = new TempFile();
|
|
|
|
$clover_tmp = null;
|
|
|
|
$clover = null;
|
|
|
|
if ($this->getEnableCoverage() !== false) {
|
|
|
|
$clover_tmp = new TempFile();
|
|
|
|
$clover = csprintf('--coverage-clover %s', $clover_tmp);
|
|
|
|
}
|
|
|
|
|
|
|
|
$config = $this->configFile ? csprintf('-c %s', $this->configFile) : null;
|
|
|
|
|
2013-08-26 18:59:51 +02:00
|
|
|
$futures[$test_path] = new ExecFuture('%C %C --log-json %s %C %s',
|
|
|
|
$this->phpunitBinary, $config, $json_tmp, $clover, $test_path);
|
2012-05-15 15:12:10 +02:00
|
|
|
$tmpfiles[$test_path] = array(
|
|
|
|
'json' => $json_tmp,
|
|
|
|
'clover' => $clover_tmp,
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
$results = array();
|
|
|
|
foreach (Futures($futures)->limit(4) as $test => $future) {
|
|
|
|
|
|
|
|
list($err, $stdout, $stderr) = $future->resolve();
|
|
|
|
|
2013-07-27 04:09:09 +02:00
|
|
|
$results[] = $this->parseTestResults(
|
|
|
|
$test,
|
2012-05-15 15:12:10 +02:00
|
|
|
$tmpfiles[$test]['json'],
|
|
|
|
$tmpfiles[$test]['clover']);
|
|
|
|
}
|
|
|
|
|
|
|
|
return array_mergev($results);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse test results from phpunit json report
|
|
|
|
*
|
|
|
|
* @param string $path Path to test
|
2013-10-20 16:41:48 +02:00
|
|
|
* @param string $json_tmp Path to phpunit json report
|
2012-05-15 15:12:10 +02:00
|
|
|
* @param string $clover_tmp Path to phpunit clover report
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function parseTestResults($path, $json_tmp, $clover_tmp) {
|
2013-01-28 02:16:50 +01:00
|
|
|
$test_results = Filesystem::readFile($json_tmp);
|
2013-01-26 02:06:20 +01:00
|
|
|
return id(new PhpunitResultParser())
|
|
|
|
->setEnableCoverage($this->getEnableCoverage())
|
|
|
|
->setProjectRoot($this->projectRoot)
|
2013-01-28 02:16:50 +01:00
|
|
|
->setCoverageFile($clover_tmp)
|
|
|
|
->setAffectedTests($this->affectedTests)
|
|
|
|
->parseTestResults($path, $test_results);
|
2012-05-15 15:12:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2012-09-30 03:03:43 +02:00
|
|
|
* Search for test cases for a given file in a large number of "reasonable"
|
|
|
|
* locations. See @{method:getSearchLocationsForTests} for specifics.
|
2012-05-15 15:12:10 +02:00
|
|
|
*
|
2012-09-30 03:03:43 +02:00
|
|
|
* TODO: Add support for finding tests in testsuite folders from
|
|
|
|
* phpunit.xml configuration.
|
|
|
|
*
|
|
|
|
* @param string PHP file to locate test cases for.
|
|
|
|
* @return string|null Path to test cases, or null.
|
|
|
|
*/
|
|
|
|
private function findTestFile($path) {
|
|
|
|
$root = $this->projectRoot;
|
|
|
|
$path = Filesystem::resolvePath($path, $root);
|
|
|
|
|
|
|
|
$file = basename($path);
|
|
|
|
$possible_files = array(
|
|
|
|
$file,
|
|
|
|
substr($file, 0, -4).'Test.php',
|
|
|
|
);
|
|
|
|
|
|
|
|
$search = self::getSearchLocationsForTests($path);
|
|
|
|
|
|
|
|
foreach ($search as $search_path) {
|
|
|
|
foreach ($possible_files as $possible_file) {
|
|
|
|
$full_path = $search_path.$possible_file;
|
|
|
|
if (!Filesystem::pathExists($full_path)) {
|
|
|
|
// If the file doesn't exist, it's clearly a miss.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!Filesystem::isDescendant($full_path, $root)) {
|
|
|
|
// Don't look above the project root.
|
|
|
|
continue;
|
|
|
|
}
|
2013-09-20 00:14:44 +02:00
|
|
|
if (0 == strcasecmp(Filesystem::resolvePath($full_path), $path)) {
|
2012-09-30 03:03:43 +02:00
|
|
|
// Don't return the original file.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return $full_path;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get places to look for PHP Unit tests that cover a given file. For some
|
|
|
|
* file "/a/b/c/X.php", we look in the same directory:
|
2012-05-15 15:12:10 +02:00
|
|
|
*
|
2012-09-30 03:03:43 +02:00
|
|
|
* /a/b/c/
|
2012-05-15 15:12:10 +02:00
|
|
|
*
|
2012-09-30 03:03:43 +02:00
|
|
|
* We then look in all parent directories for a directory named "tests/"
|
|
|
|
* (or "Tests/"):
|
2012-05-15 15:12:10 +02:00
|
|
|
*
|
2012-09-30 03:03:43 +02:00
|
|
|
* /a/b/c/tests/
|
|
|
|
* /a/b/tests/
|
|
|
|
* /a/tests/
|
|
|
|
* /tests/
|
|
|
|
*
|
|
|
|
* We also try to replace each directory component with "tests/":
|
2012-05-15 15:12:10 +02:00
|
|
|
*
|
2012-09-30 03:03:43 +02:00
|
|
|
* /a/b/tests/
|
|
|
|
* /a/tests/c/
|
|
|
|
* /tests/b/c/
|
2012-05-15 15:12:10 +02:00
|
|
|
*
|
2012-09-30 03:03:43 +02:00
|
|
|
* We also try to add "tests/" at each directory level:
|
|
|
|
*
|
|
|
|
* /a/b/c/tests/
|
|
|
|
* /a/b/tests/c/
|
|
|
|
* /a/tests/b/c/
|
|
|
|
* /tests/a/b/c/
|
|
|
|
*
|
|
|
|
* This finds tests with a layout like:
|
|
|
|
*
|
|
|
|
* docs/
|
|
|
|
* src/
|
|
|
|
* tests/
|
|
|
|
*
|
|
|
|
* ...or similar. This list will be further pruned by the caller; it is
|
|
|
|
* intentionally filesystem-agnostic to be unit testable.
|
|
|
|
*
|
|
|
|
* @param string PHP file to locate test cases for.
|
|
|
|
* @return list<string> List of directories to search for tests in.
|
2012-05-15 15:12:10 +02:00
|
|
|
*/
|
2012-09-30 03:03:43 +02:00
|
|
|
public static function getSearchLocationsForTests($path) {
|
|
|
|
$file = basename($path);
|
|
|
|
$dir = dirname($path);
|
|
|
|
|
|
|
|
$test_dir_names = array('tests', 'Tests');
|
|
|
|
|
|
|
|
$try_directories = array();
|
|
|
|
|
|
|
|
// Try in the current directory.
|
|
|
|
$try_directories[] = array($dir);
|
|
|
|
|
|
|
|
// Try in a tests/ directory anywhere in the ancestry.
|
|
|
|
foreach (Filesystem::walkToRoot($dir) as $parent_dir) {
|
|
|
|
if ($parent_dir == '/') {
|
|
|
|
// We'll restore this later.
|
|
|
|
$parent_dir = '';
|
|
|
|
}
|
|
|
|
foreach ($test_dir_names as $test_dir_name) {
|
|
|
|
$try_directories[] = array($parent_dir, $test_dir_name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try replacing each directory component with 'tests/'.
|
|
|
|
$parts = trim($dir, DIRECTORY_SEPARATOR);
|
|
|
|
$parts = explode(DIRECTORY_SEPARATOR, $parts);
|
|
|
|
foreach (array_reverse(array_keys($parts)) as $key) {
|
|
|
|
foreach ($test_dir_names as $test_dir_name) {
|
|
|
|
$try = $parts;
|
|
|
|
$try[$key] = $test_dir_name;
|
|
|
|
array_unshift($try, '');
|
|
|
|
$try_directories[] = $try;
|
2012-05-15 15:12:10 +02:00
|
|
|
}
|
2012-09-30 03:03:43 +02:00
|
|
|
}
|
2012-05-15 15:12:10 +02:00
|
|
|
|
2012-09-30 03:03:43 +02:00
|
|
|
// Try adding 'tests/' at each level.
|
|
|
|
foreach (array_reverse(array_keys($parts)) as $key) {
|
|
|
|
foreach ($test_dir_names as $test_dir_name) {
|
|
|
|
$try = $parts;
|
|
|
|
$try[$key] = $test_dir_name.DIRECTORY_SEPARATOR.$try[$key];
|
|
|
|
array_unshift($try, '');
|
|
|
|
$try_directories[] = $try;
|
2012-05-15 15:12:10 +02:00
|
|
|
}
|
2012-09-30 03:03:43 +02:00
|
|
|
}
|
2012-05-15 15:12:10 +02:00
|
|
|
|
2012-09-30 03:03:43 +02:00
|
|
|
$results = array();
|
|
|
|
foreach ($try_directories as $parts) {
|
|
|
|
$results[implode(DIRECTORY_SEPARATOR, $parts).DIRECTORY_SEPARATOR] = true;
|
2012-05-15 15:12:10 +02:00
|
|
|
}
|
|
|
|
|
2012-09-30 03:03:43 +02:00
|
|
|
return array_keys($results);
|
2012-05-15 15:12:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tries to find and update phpunit configuration file
|
|
|
|
* based on phpunit_config option in .arcconfig
|
|
|
|
*/
|
|
|
|
private function prepareConfigFile() {
|
|
|
|
$project_root = $this->projectRoot . DIRECTORY_SEPARATOR;
|
2013-10-22 22:58:32 +02:00
|
|
|
$config = $this->getConfigurationManager()->getConfigFromAnySource(
|
|
|
|
'phpunit_config');
|
2012-05-15 15:12:10 +02:00
|
|
|
|
2013-10-22 22:58:32 +02:00
|
|
|
if ($config) {
|
2012-05-15 15:12:10 +02:00
|
|
|
if (Filesystem::pathExists($project_root . $config)) {
|
|
|
|
$this->configFile = $project_root . $config;
|
|
|
|
} else {
|
|
|
|
throw new Exception('PHPUnit configuration file was not ' .
|
|
|
|
'found in ' . $project_root . $config);
|
|
|
|
}
|
|
|
|
}
|
2013-10-22 22:58:32 +02:00
|
|
|
$bin = $this->getConfigurationManager()->getConfigFromAnySource(
|
|
|
|
'unit.phpunit.binary');
|
|
|
|
if ($bin) {
|
2013-08-26 18:59:51 +02:00
|
|
|
if (Filesystem::binaryExists($bin)) {
|
|
|
|
$this->phpunitBinary = $bin;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$this->phpunitBinary = Filesystem::resolvePath($bin, $project_root);
|
|
|
|
}
|
|
|
|
}
|
2012-05-15 15:12:10 +02:00
|
|
|
}
|
|
|
|
}
|