Changeset View
Changeset View
Standalone View
Standalone View
src/unit/engine/PhpunitTestEngine.php
- This file was copied to src/unit/engine/ArcanistPhpunitTestEngine.php.
| <?php | <?php | ||||
| /** | /** | ||||
| * PHPUnit wrapper. | * @deprecated | ||||
| */ | */ | ||||
| final class PhpunitTestEngine extends ArcanistUnitTestEngine { | final class PhpunitTestEngine extends ArcanistPhpunitTestEngine { | ||||
| private $configFile; | |||||
| private $phpunitBinary = 'phpunit'; | |||||
| private $affectedTests; | |||||
| private $projectRoot; | |||||
| public function run() { | public function run() { | ||||
| $this->projectRoot = $this->getWorkingCopy()->getProjectRoot(); | phutil_deprecated( | ||||
| $this->affectedTests = array(); | __CLASS__, | ||||
| foreach ($this->getPaths() as $path) { | |||||
| $path = Filesystem::resolvePath($path, $this->projectRoot); | |||||
| // 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(pht('No tests to run.')); | |||||
| } | |||||
| $this->prepareConfigFile(); | |||||
| $futures = array(); | |||||
| $tmpfiles = array(); | |||||
| foreach ($this->affectedTests as $class_path => $test_path) { | |||||
| if (!Filesystem::pathExists($test_path)) { | |||||
| continue; | |||||
| } | |||||
| $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; | |||||
| $stderr = '-d display_errors=stderr'; | |||||
| $futures[$test_path] = new ExecFuture('%C %C %C --log-json %s %C %s', | |||||
| $this->phpunitBinary, $config, $stderr, $json_tmp, $clover, $test_path); | |||||
| $tmpfiles[$test_path] = array( | |||||
| 'json' => $json_tmp, | |||||
| 'clover' => $clover_tmp, | |||||
| ); | |||||
| } | |||||
| $results = array(); | |||||
| $futures = id(new FutureIterator($futures)) | |||||
| ->limit(4); | |||||
| foreach ($futures as $test => $future) { | |||||
| list($err, $stdout, $stderr) = $future->resolve(); | |||||
| $results[] = $this->parseTestResults( | |||||
| $test, | |||||
| $tmpfiles[$test]['json'], | |||||
| $tmpfiles[$test]['clover'], | |||||
| $stderr); | |||||
| } | |||||
| return array_mergev($results); | |||||
| } | |||||
| /** | |||||
| * Parse test results from phpunit json report. | |||||
| * | |||||
| * @param string $path Path to test | |||||
| * @param string $json_tmp Path to phpunit json report | |||||
| * @param string $clover_tmp Path to phpunit clover report | |||||
| * @param string $stderr Data written to stderr | |||||
| * | |||||
| * @return array | |||||
| */ | |||||
| private function parseTestResults($path, $json_tmp, $clover_tmp, $stderr) { | |||||
| $test_results = Filesystem::readFile($json_tmp); | |||||
| return id(new ArcanistPhpunitTestResultParser()) | |||||
| ->setEnableCoverage($this->getEnableCoverage()) | |||||
| ->setProjectRoot($this->projectRoot) | |||||
| ->setCoverageFile($clover_tmp) | |||||
| ->setAffectedTests($this->affectedTests) | |||||
| ->setStderr($stderr) | |||||
| ->parseTestResults($path, $test_results); | |||||
| } | |||||
| /** | |||||
| * Search for test cases for a given file in a large number of "reasonable" | |||||
| * locations. See @{method:getSearchLocationsForTests} for specifics. | |||||
| * | |||||
| * 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; | |||||
| } | |||||
| if (0 == strcasecmp(Filesystem::resolvePath($full_path), $path)) { | |||||
| // 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: | |||||
| * | |||||
| * /a/b/c/ | |||||
| * | |||||
| * We then look in all parent directories for a directory named "tests/" | |||||
| * (or "Tests/"): | |||||
| * | |||||
| * /a/b/c/tests/ | |||||
| * /a/b/tests/ | |||||
| * /a/tests/ | |||||
| * /tests/ | |||||
| * | |||||
| * We also try to replace each directory component with "tests/": | |||||
| * | |||||
| * /a/b/tests/ | |||||
| * /a/tests/c/ | |||||
| * /tests/b/c/ | |||||
| * | |||||
| * 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. | |||||
| */ | |||||
| 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; | |||||
| } | |||||
| } | |||||
| // 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; | |||||
| } | |||||
| } | |||||
| $results = array(); | |||||
| foreach ($try_directories as $parts) { | |||||
| $results[implode(DIRECTORY_SEPARATOR, $parts).DIRECTORY_SEPARATOR] = true; | |||||
| } | |||||
| return array_keys($results); | |||||
| } | |||||
| /** | |||||
| * Tries to find and update phpunit configuration file based on | |||||
| * `phpunit_config` option in `.arcconfig`. | |||||
| */ | |||||
| private function prepareConfigFile() { | |||||
| $project_root = $this->projectRoot.DIRECTORY_SEPARATOR; | |||||
| $config = $this->getConfigurationManager()->getConfigFromAnySource( | |||||
| 'phpunit_config'); | |||||
| if ($config) { | |||||
| if (Filesystem::pathExists($project_root.$config)) { | |||||
| $this->configFile = $project_root.$config; | |||||
| } else { | |||||
| throw new Exception( | |||||
| pht( | pht( | ||||
| 'PHPUnit configuration file was not found in %s', | 'You should use `%s` instead.', | ||||
| $project_root.$config)); | 'ArcanistPhpunitTestEngine')); | ||||
| } | parent::run(); | ||||
| } | |||||
| $bin = $this->getConfigurationManager()->getConfigFromAnySource( | |||||
| 'unit.phpunit.binary'); | |||||
| if ($bin) { | |||||
| if (Filesystem::binaryExists($bin)) { | |||||
| $this->phpunitBinary = $bin; | |||||
| } else { | |||||
| $this->phpunitBinary = Filesystem::resolvePath($bin, $project_root); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||