diff --git a/src/workflow/ArcanistUnitWorkflow.php b/src/workflow/ArcanistUnitWorkflow.php index f302ba75..d2ccdd48 100644 --- a/src/workflow/ArcanistUnitWorkflow.php +++ b/src/workflow/ArcanistUnitWorkflow.php @@ -1,356 +1,360 @@ array( 'param' => 'revision', 'help' => 'Run unit tests covering changes since a specific revision.', 'supports' => array( 'git', 'hg', ), 'nosupport' => array( 'svn' => 'Arc unit does not currently support --rev in SVN.', ), ), 'engine' => array( 'param' => 'classname', 'help' => 'Override configured unit engine for this project.', ), 'coverage' => array( 'help' => 'Always enable coverage information.', 'conflicts' => array( 'no-coverage' => null, ), ), 'no-coverage' => array( 'help' => 'Always disable coverage information.', ), 'detailed-coverage' => array( 'help' => 'Show a detailed coverage report on the CLI. Implies '. '--coverage.', ), 'json' => array( 'help' => 'Report results in JSON format.', ), 'output' => array( 'param' => 'format', 'help' => "With 'full', show full pretty report (Default). ". "With 'json', report results in JSON format. ". "With 'ugly', use uglier (but more efficient) JSON formatting. ". "With 'none', don't print results. ", 'conflicts' => array( 'json' => 'Only one output format allowed', 'ugly' => 'Only one output format allowed', ), ), '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', ); } public function requiresWorkingCopy() { return true; } public function requiresRepositoryAPI() { return true; } public function getEngine() { return $this->engine; } public function run() { $working_copy = $this->getWorkingCopy(); $engine_class = $this->getArgument( 'engine', $this->getConfigurationManager()->getConfigFromAnySource('unit.engine')); if (!$engine_class) { throw new ArcanistNoEngineException( 'No unit test engine is configured for this project. Edit .arcconfig '. 'to specify a unit test engine.'); } $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); + if ($everything) { + $paths = iterator_to_array($this->getRepositoryApi()->getAllFiles()); + } else { + $paths = $this->selectPathsForWorkflow($paths, $rev); + } if (!class_exists($engine_class) || !is_subclass_of($engine_class, 'ArcanistUnitTestEngine')) { throw new ArcanistUsageException( "Configured unit test engine '{$engine_class}' is not a subclass of ". "'ArcanistUnitTestEngine'."); } $this->engine = newv($engine_class, array()); $this->engine->setWorkingCopy($working_copy); $this->engine->setConfigurationManager($this->getConfigurationManager()); if ($everything) { $this->engine->setRunAllTests(true); } else { $this->engine->setPaths($paths); } $this->engine->setArguments($this->getPassthruArgumentsAsMap('unit')); $renderer = new ArcanistUnitConsoleRenderer(); $this->engine->setRenderer($renderer); $enable_coverage = null; // Means "default". if ($this->getArgument('coverage') || $this->getArgument('detailed-coverage')) { $enable_coverage = true; } else if ($this->getArgument('no-coverage')) { $enable_coverage = false; } $this->engine->setEnableCoverage($enable_coverage); // Enable possible async tests only for 'arc diff' not 'arc unit' if ($this->getParentWorkflow()) { $this->engine->setEnableAsyncTests(true); } else { $this->engine->setEnableAsyncTests(false); } $results = $this->engine->run(); $this->testResults = $results; $console = PhutilConsole::getConsole(); $output_format = $this->getOutputFormat(); if ($output_format !== 'full') { $console->disableOut(); } $unresolved = array(); $coverage = array(); $postponed_count = 0; foreach ($results as $result) { $result_code = $result->getResult(); if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED) { $postponed_count++; $unresolved[] = $result; } else { if ($this->engine->shouldEchoTestResults()) { $console->writeOut('%s', $renderer->renderUnitResult($result)); } if ($result_code != ArcanistUnitTestResult::RESULT_PASS) { $unresolved[] = $result; } } if ($result->getCoverage()) { foreach ($result->getCoverage() as $file => $report) { $coverage[$file][] = $report; } } } if ($postponed_count) { $console->writeOut( '%s', $renderer->renderPostponedResult($postponed_count)); } if ($coverage) { $file_coverage = array_fill_keys( $paths, 0); $file_reports = array(); foreach ($coverage as $file => $reports) { $report = ArcanistUnitTestResult::mergeCoverage($reports); $cov = substr_count($report, 'C'); $uncov = substr_count($report, 'U'); if ($cov + $uncov) { $coverage = $cov / ($cov + $uncov); } else { $coverage = 0; } $file_coverage[$file] = $coverage; $file_reports[$file] = $report; } $console->writeOut("\n__COVERAGE REPORT__\n"); asort($file_coverage); foreach ($file_coverage as $file => $coverage) { $console->writeOut( " **%s%%** %s\n", sprintf('% 3d', (int)(100 * $coverage)), $file); $full_path = $working_copy->getProjectRoot().'/'.$file; if ($this->getArgument('detailed-coverage') && Filesystem::pathExists($full_path) && is_file($full_path) && array_key_exists($file, $file_reports)) { $console->writeOut( '%s', $this->renderDetailedCoverageReport( Filesystem::readFile($full_path), $file_reports[$file])); } } } $this->unresolvedTests = $unresolved; $overall_result = self::RESULT_OKAY; foreach ($results as $result) { $result_code = $result->getResult(); if ($result_code == ArcanistUnitTestResult::RESULT_FAIL || $result_code == ArcanistUnitTestResult::RESULT_BROKEN) { $overall_result = self::RESULT_FAIL; break; } else if ($result_code == ArcanistUnitTestResult::RESULT_UNSOUND) { $overall_result = self::RESULT_UNSOUND; } else if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED && $overall_result != self::RESULT_UNSOUND) { $overall_result = self::RESULT_POSTPONED; } } if ($output_format !== 'full') { $console->enableOut(); } $data = array_values(mpull($results, 'toDictionary')); switch ($output_format) { case 'ugly': $console->writeOut('%s', json_encode($data)); break; case 'json': $json = new PhutilJSON(); $console->writeOut('%s', $json->encodeFormatted($data)); break; case 'full': // already printed break; case 'none': // do nothing break; } return $overall_result; } public function getUnresolvedTests() { return $this->unresolvedTests; } public function getTestResults() { return $this->testResults; } private function renderDetailedCoverageReport($data, $report) { $data = explode("\n", $data); $out = ''; $n = 0; foreach ($data as $line) { $out .= sprintf('% 5d ', $n + 1); $line = str_pad($line, 80, ' '); if (empty($report[$n])) { $c = 'N'; } else { $c = $report[$n]; } switch ($c) { case 'C': $out .= phutil_console_format( ' %s ', $line); break; case 'U': $out .= phutil_console_format( ' %s ', $line); break; case 'X': $out .= phutil_console_format( ' %s ', $line); break; default: $out .= ' '.$line.' '; break; } $out .= "\n"; $n++; } return $out; } private function getOutputFormat() { if ($this->getArgument('ugly')) { return 'ugly'; } if ($this->getArgument('json')) { return 'json'; } $format = $this->getArgument('output'); $known_formats = array( 'none' => 'none', 'json' => 'json', 'ugly' => 'ugly', 'full' => 'full', ); return idx($known_formats, $format, 'full'); } }