array( 'param' => 'classname', 'help' => "Override configured unit engine for this project." ), '*' => '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', $working_copy->getConfig('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."); } $repository_api = $this->getRepositoryAPI(); if ($this->getArgument('paths')) { // TODO: deal with git stuff $paths = $this->getArgument('paths'); } else { $paths = $repository_api->getWorkingCopyStatus(); // TODO: clean this up foreach ($paths as $path => $mask) { if ($mask & ArcanistRepositoryAPI::FLAG_UNTRACKED) { unset($paths[$path]); } } $paths = array_keys($paths); } PhutilSymbolLoader::loadClass($engine_class); $this->engine = newv($engine_class, array()); $this->engine->setWorkingCopy($working_copy); $this->engine->setPaths($paths); $this->engine->setArguments($this->getPassthruArgumentsAsMap('unit')); // 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(); $status_codes = array( ArcanistUnitTestResult::RESULT_PASS => phutil_console_format( '** PASS **'), ArcanistUnitTestResult::RESULT_FAIL => phutil_console_format( '** FAIL **'), ArcanistUnitTestResult::RESULT_SKIP => phutil_console_format( '** SKIP **'), ArcanistUnitTestResult::RESULT_BROKEN => phutil_console_format( '** BROKEN **'), ArcanistUnitTestResult::RESULT_UNSOUND => phutil_console_format( '** UNSOUND **'), ArcanistUnitTestResult::RESULT_POSTPONED => phutil_console_format( '** POSTPONED **'), ); $unresolved = 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()) { echo ' '.$status_codes[$result_code]; if ($result_code == ArcanistUnitTestResult::RESULT_PASS) { echo ' '.self::formatTestDuration($result->getDuration()); } echo ' '.$result->getName()."\n"; } if ($result_code != ArcanistUnitTestResult::RESULT_PASS) { if ($this->engine->shouldEchoTestResults()) { echo $result->getUserData()."\n"; } $unresolved[] = $result; } } } if ($postponed_count) { echo sprintf("%s %d %s\n", $status_codes[ArcanistUnitTestResult::RESULT_POSTPONED], $postponed_count, ($postponed_count > 1)?'tests':'test'); } $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; } } return $overall_result; } public function getUnresolvedTests() { return $this->unresolvedTests; } public function setDifferentialDiffID($id) { if ($this->engine) { $this->engine->setDifferentialDiffID($id); } } private static function formatTestDuration($seconds) { // Very carefully define inclusive upper bounds on acceptable unit test // durations. Times are in milliseconds and are in increasing order. $acceptableness = array( 50 => "%s\xE2\x98\x85 ", 200 => '%s ', 500 => '%s ', INF => '%s ', ); $milliseconds = $seconds * 1000; $duration = self::formatTime($seconds); foreach ($acceptableness as $upper_bound => $formatting) { if ($milliseconds <= $upper_bound) { return phutil_console_format($formatting, $duration); } } return phutil_console_format(end($acceptableness), $duration); } private static function formatTime($seconds) { if ($seconds >= 60) { $minutes = floor($seconds / 60); return sprintf('%dm%02ds', $minutes, round($seconds % 60)); } if ($seconds >= 1) { return sprintf('%4.1fs', $seconds); } $milliseconds = $seconds * 1000; if ($milliseconds >= 1) { return sprintf('%3dms', round($milliseconds)); } return ' <1ms'; } }