diff --git a/src/unit/ArcanistUnitTestResult.php b/src/unit/ArcanistUnitTestResult.php index 7e6efc29..2690d6ee 100644 --- a/src/unit/ArcanistUnitTestResult.php +++ b/src/unit/ArcanistUnitTestResult.php @@ -125,4 +125,16 @@ final class ArcanistUnitTestResult { return $base; } + public function toDictionary() { + return array( + 'name' => $this->getName(), + 'link' => $this->getLink(), + 'result' => $this->getResult(), + 'duration' => $this->getDuration(), + 'extra' => $this->getExtraData(), + 'userData' => $this->getUserData(), + 'coverage' => $this->getCoverage(), + ); + } + } diff --git a/src/unit/engine/ArcanistBaseUnitTestEngine.php b/src/unit/engine/ArcanistBaseUnitTestEngine.php index 12416796..e6ea50f0 100644 --- a/src/unit/engine/ArcanistBaseUnitTestEngine.php +++ b/src/unit/engine/ArcanistBaseUnitTestEngine.php @@ -13,6 +13,27 @@ abstract class ArcanistBaseUnitTestEngine { protected $diffID; private $enableAsyncTests; private $enableCoverage; + private $runAllTests; + + + public function setRunAllTests($run_all_tests) { + if (!$this->supportsRunAllTests() && $run_all_tests) { + $class = get_class($this); + throw new Exception( + "Engine '{$class}' does not support --everything."); + } + + $this->runAllTests = $run_all_tests; + return $this; + } + + public function getRunAllTests() { + return $this->runAllTests; + } + + protected function supportsRunAllTests() { + return false; + } final public function __construct() { diff --git a/src/unit/engine/PhutilUnitTestEngine.php b/src/unit/engine/PhutilUnitTestEngine.php index 8e02772c..9f1cfcbb 100644 --- a/src/unit/engine/PhutilUnitTestEngine.php +++ b/src/unit/engine/PhutilUnitTestEngine.php @@ -7,9 +7,86 @@ */ final class PhutilUnitTestEngine extends ArcanistBaseUnitTestEngine { - public function run() { + protected function supportsRunAllTests() { + return true; + } - $bootloader = PhutilBootloader::getInstance(); + public function run() { + if ($this->getRunAllTests()) { + $run_tests = $this->getAllTests(); + } else { + $run_tests = $this->getTestsForPaths(); + } + + if (!$run_tests) { + throw new ArcanistNoEffectException("No tests to run."); + } + + $enable_coverage = $this->getEnableCoverage(); + if ($enable_coverage !== false) { + if (!function_exists('xdebug_start_code_coverage')) { + if ($enable_coverage === true) { + throw new ArcanistUsageException( + "You specified --coverage but xdebug is not available, so ". + "coverage can not be enabled for PhutilUnitTestEngine."); + } + } else { + $enable_coverage = true; + } + } + + $project_root = $this->getWorkingCopy()->getProjectRoot(); + + $results = array(); + foreach ($run_tests as $test_class) { + $test_case = newv($test_class, array()); + $test_case->setEnableCoverage($enable_coverage); + $test_case->setProjectRoot($project_root); + if ($this->getPaths()) { + $test_case->setPaths($this->getPaths()); + } + $results[] = $test_case->run(); + } + + $results = array_mergev($results); + return $results; + } + + private function getAllTests() { + $project_root = $this->getWorkingCopy()->getProjectRoot(); + + $symbols = id(new PhutilSymbolLoader()) + ->setType('class') + ->setAncestorClass('ArcanistPhutilTestCase') + ->setConcreteOnly(true) + ->selectSymbolsWithoutLoading(); + + $in_working_copy = array(); + + $run_tests = array(); + foreach ($symbols as $symbol) { + if (!preg_match('@/__tests__/@', $symbol['where'])) { + continue; + } + + $library = $symbol['library']; + + if (!isset($in_working_copy[$library])) { + $library_root = phutil_get_library_root($library); + $in_working_copy[$library] = Filesystem::isDescendant( + $library_root, + $project_root); + } + + if ($in_working_copy[$library]) { + $run_tests[] = $symbol['name']; + } + } + + return $run_tests; + } + + private function getTestsForPaths() { $project_root = $this->getWorkingCopy()->getProjectRoot(); $look_here = array(); @@ -89,34 +166,7 @@ final class PhutilUnitTestEngine extends ArcanistBaseUnitTestEngine { } $run_tests = array_keys($run_tests); - if (!$run_tests) { - throw new ArcanistNoEffectException("No tests to run."); - } - - $enable_coverage = $this->getEnableCoverage(); - if ($enable_coverage !== false) { - if (!function_exists('xdebug_start_code_coverage')) { - if ($enable_coverage === true) { - throw new ArcanistUsageException( - "You specified --coverage but xdebug is not available, so ". - "coverage can not be enabled for PhutilUnitTestEngine."); - } - } else { - $enable_coverage = true; - } - } - - $results = array(); - foreach ($run_tests as $test_class) { - $test_case = newv($test_class, array()); - $test_case->setEnableCoverage($enable_coverage); - $test_case->setProjectRoot($project_root); - $test_case->setPaths($this->getPaths()); - $results[] = $test_case->run(); - } - - $results = array_mergev($results); - return $results; + return $run_tests; } } diff --git a/src/workflow/ArcanistUnitWorkflow.php b/src/workflow/ArcanistUnitWorkflow.php index 7da6d268..37c2e321 100644 --- a/src/workflow/ArcanistUnitWorkflow.php +++ b/src/workflow/ArcanistUnitWorkflow.php @@ -69,6 +69,18 @@ EOTEXT 'help' => "Show a detailed coverage report on the CLI. Implies ". "--coverage.", ), + 'json' => array( + 'help' => 'Report results in JSON format.', + ), + 'everything' => array( + 'help' => 'Run every test.', + 'conflicts' => array( + 'rev' => '--everything runs all tests.', + ), + ), + 'ugly' => array( + 'help' => 'With --json, use uglier (but more efficient) formatting.', + ), '*' => 'paths', ); } @@ -101,6 +113,12 @@ EOTEXT $paths = $this->getArgument('paths'); $rev = $this->getArgument('rev'); + $everything = $this->getArgument('everything'); + if ($everything && $paths) { + throw new ArcanistUsageException( + "You can not specify paths with --everything. The --everything ". + "flag runs every test."); + } $paths = $this->selectPathsForWorkflow($paths, $rev); @@ -113,7 +131,11 @@ EOTEXT $this->engine = newv($engine_class, array()); $this->engine->setWorkingCopy($working_copy); - $this->engine->setPaths($paths); + if ($everything) { + $this->engine->setRunAllTests(true); + } else { + $this->engine->setPaths($paths); + } $this->engine->setArguments($this->getPassthruArgumentsAsMap('unit')); $enable_coverage = null; // Means "default". @@ -137,6 +159,12 @@ EOTEXT $console = PhutilConsole::getConsole(); + $json_output = $this->getArgument('json'); + + if ($json_output) { + $console->disableOut(); + } + $unresolved = array(); $coverage = array(); $postponed_count = 0; @@ -234,6 +262,19 @@ EOTEXT } } + if ($json_output) { + $console->enableOut(); + + $data = array_values(mpull($results, 'toDictionary')); + + if ($this->getArgument('ugly')) { + $console->writeOut('%s', json_encode($data)); + } else { + $json = new PhutilJSON(); + $console->writeOut('%s', $json->encodeFormatted($data)); + } + } + return $overall_result; }