diff --git a/src/configuration/ArcanistConfigurationManager.php b/src/configuration/ArcanistConfigurationManager.php index d2284742..3473a0bd 100644 --- a/src/configuration/ArcanistConfigurationManager.php +++ b/src/configuration/ArcanistConfigurationManager.php @@ -1,344 +1,346 @@ workingCopy = $working_copy; + return $this; } /* -( Get config )--------------------------------------------------------- */ const CONFIG_SOURCE_RUNTIME = 'runtime'; const CONFIG_SOURCE_LOCAL = 'local'; const CONFIG_SOURCE_PROJECT = 'project'; const CONFIG_SOURCE_USER = 'user'; const CONFIG_SOURCE_SYSTEM = 'system'; const CONFIG_SOURCE_DEFAULT = 'default'; public function getProjectConfig($key) { if ($this->workingCopy) { return $this->workingCopy->getProjectConfig($key); } return null; } public function getLocalConfig($key) { if ($this->workingCopy) { return $this->workingCopy->getLocalConfig($key); } return null; } public function getWorkingCopyIdentity() { return $this->workingCopy; } /** * Read a configuration directive from any available configuration source. * This includes the directive in local, user and system configuration in * addition to project configuration, and configuration provided as command * arguments ("runtime"). * The precedence is runtime > local > project > user > system * * @param key Key to read. * @param wild Default value if key is not found. * @return wild Value, or default value if not found. * * @task config */ public function getConfigFromAnySource($key, $default = null) { $all = $this->getConfigFromAllSources($key); return empty($all) ? $default : head($all); } /** * For the advanced case where you want customized configuration handling. * * Reads the configuration from all available sources, returning a map (array) * of results, with the source as key. Missing values will not be in the map, * so an empty array will be returned if no results are found. * * The map is ordered by the canonical sources precedence, which is: * runtime > local > project > user > system * * @param key Key to read * @return array Mapping of source => value read. Sources with no value are * not in the array. * * @task config */ public function getConfigFromAllSources($key) { $results = array(); $settings = new ArcanistSettings(); $pval = idx($this->runtimeConfig, $key); if ($pval !== null) { $results[self::CONFIG_SOURCE_RUNTIME] = $settings->willReadValue($key, $pval); } $pval = $this->getLocalConfig($key); if ($pval !== null) { $results[self::CONFIG_SOURCE_LOCAL] = $settings->willReadValue($key, $pval); } $pval = $this->getProjectConfig($key); if ($pval !== null) { $results[self::CONFIG_SOURCE_PROJECT] = $settings->willReadValue($key, $pval); } $user_config = $this->readUserArcConfig(); $pval = idx($user_config, $key); if ($pval !== null) { $results[self::CONFIG_SOURCE_USER] = $settings->willReadValue($key, $pval); } $system_config = $this->readSystemArcConfig(); $pval = idx($system_config, $key); if ($pval !== null) { $results[self::CONFIG_SOURCE_SYSTEM] = $settings->willReadValue($key, $pval); } $default_config = $this->readDefaultConfig(); if (array_key_exists($key, $default_config)) { $results[self::CONFIG_SOURCE_DEFAULT] = $default_config[$key]; } return $results; } /** * Sets a runtime config value that takes precedence over any static * config values. * * @param key Key to set. * @param value The value of the key. * * @task config */ public function setRuntimeConfig($key, $value) { $this->runtimeConfig[$key] = $value; return $this; } /* -( Read/write config )--------------------------------------------------- */ public function readLocalArcConfig() { if ($this->workingCopy) { return $this->workingCopy->readLocalArcConfig(); } return array(); } public function writeLocalArcConfig(array $config) { if ($this->workingCopy) { return $this->workingCopy->writeLocalArcConfig($config); } throw new Exception(pht('No working copy to write config to!')); } /** * This is probably not the method you're looking for; try * @{method:readUserArcConfig}. */ public function readUserConfigurationFile() { if ($this->userConfigCache === null) { $user_config = array(); $user_config_path = $this->getUserConfigurationFileLocation(); $console = PhutilConsole::getConsole(); if (Filesystem::pathExists($user_config_path)) { $console->writeLog( "%s\n", pht( 'Config: Reading user configuration file "%s"...', $user_config_path)); if (!phutil_is_windows()) { $mode = fileperms($user_config_path); if (!$mode) { throw new Exception( pht( 'Unable to read file permissions for "%s"!', $user_config_path)); } if ($mode & 0177) { // Mode should allow only owner access. $prompt = pht( "File permissions on your %s are too open. ". "Fix them by chmod'ing to 600?", '~/.arcrc'); if (!phutil_console_confirm($prompt, $default_no = false)) { throw new ArcanistUsageException( pht('Set %s to file mode 600.', '~/.arcrc')); } execx('chmod 600 %s', $user_config_path); // Drop the stat cache so we don't read the old permissions if // we end up here again. If we don't do this, we may prompt the user // to fix permissions multiple times. clearstatcache(); } } $user_config_data = Filesystem::readFile($user_config_path); try { $user_config = phutil_json_decode($user_config_data); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht("Your '%s' file is not a valid JSON file.", '~/.arcrc'), $ex); } } else { $console->writeLog( "%s\n", pht( 'Config: Did not find user configuration at "%s".', $user_config_path)); } $this->userConfigCache = $user_config; } return $this->userConfigCache; } /** * This is probably not the method you're looking for; try * @{method:writeUserArcConfig}. */ public function writeUserConfigurationFile($config) { $json_encoder = new PhutilJSON(); $json = $json_encoder->encodeFormatted($config); $path = $this->getUserConfigurationFileLocation(); Filesystem::writeFile($path, $json); if (!phutil_is_windows()) { execx('chmod 600 %s', $path); } } public function setUserConfigurationFileLocation($custom_arcrc) { if (!Filesystem::pathExists($custom_arcrc)) { throw new Exception( pht('Custom %s file was specified, but it was not found!', 'arcrc')); } $this->customArcrcFilename = $custom_arcrc; $this->userConfigCache = null; + return $this; } public function getUserConfigurationFileLocation() { if (strlen($this->customArcrcFilename)) { return $this->customArcrcFilename; } if (phutil_is_windows()) { return getenv('APPDATA').'/.arcrc'; } else { return getenv('HOME').'/.arcrc'; } } public function readUserArcConfig() { return idx($this->readUserConfigurationFile(), 'config', array()); } public function writeUserArcConfig(array $options) { $config = $this->readUserConfigurationFile(); $config['config'] = $options; $this->writeUserConfigurationFile($config); } public function getSystemArcConfigLocation() { if (phutil_is_windows()) { return Filesystem::resolvePath( 'Phabricator/Arcanist/config', getenv('ProgramData')); } else { return '/etc/arcconfig'; } } public function readSystemArcConfig() { static $system_config; if ($system_config === null) { $system_config = array(); $system_config_path = $this->getSystemArcConfigLocation(); $console = PhutilConsole::getConsole(); if (Filesystem::pathExists($system_config_path)) { $console->writeLog( "%s\n", pht( 'Config: Reading system configuration file "%s"...', $system_config_path)); $file = Filesystem::readFile($system_config_path); try { $system_config = phutil_json_decode($file); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht( "Your '%s' file is not a valid JSON file.", $system_config_path), $ex); } } else { $console->writeLog( "%s\n", pht( 'Config: Did not find system configuration at "%s".', $system_config_path)); } } return $system_config; } public function applyRuntimeArcConfig($args) { $arcanist_settings = new ArcanistSettings(); $options = $args->getArg('config'); foreach ($options as $opt) { $opt_config = preg_split('/=/', $opt, 2); if (count($opt_config) !== 2) { throw new ArcanistUsageException( pht( "Argument was '%s', but must be '%s'. For example, %s", $opt, 'name=value', 'history.immutable=true')); } list($key, $value) = $opt_config; $value = $arcanist_settings->willWriteValue($key, $value); $this->setRuntimeConfig($key, $value); } return $this->runtimeConfig; } public function readDefaultConfig() { $settings = new ArcanistSettings(); return $settings->getDefaultSettings(); } } diff --git a/src/lint/linter/xhpast/ArcanistXHPASTLinterRule.php b/src/lint/linter/xhpast/ArcanistXHPASTLinterRule.php index 00c44194..b98636f6 100644 --- a/src/lint/linter/xhpast/ArcanistXHPASTLinterRule.php +++ b/src/lint/linter/xhpast/ArcanistXHPASTLinterRule.php @@ -1,193 +1,194 @@ setAncestorClass(__CLASS__) ->loadObjects(); foreach ($symbols as $class => $rule) { $id = $rule->getLintID(); if (isset($rules[$id])) { throw new Exception( pht( 'Two linter rules (`%s`, `%s`) share the same lint ID (%d). '. 'Each linter rule must have a unique ID.', $class, get_class($rules[$id]), $id)); } $rules[$id] = $rule; } return $rules; } final public function getLintID() { $class = new ReflectionClass($this); $const = $class->getConstant('ID'); if ($const === false) { throw new Exception( pht( '`%s` class `%s` must define an ID constant.', __CLASS__, get_class($this))); } if (!is_int($const)) { throw new Exception( pht( '`%s` class `%s` has an invalid ID constant. ID must be an integer.', __CLASS__, get_class($this))); } return $const; } abstract public function getLintName(); public function getLintSeverity() { return ArcanistLintSeverity::SEVERITY_ERROR; } public function getLinterConfigurationOptions() { return array(); } public function setLinterConfigurationValue($key, $value) {} abstract public function process(XHPASTNode $root); final public function setLinter(ArcanistXHPASTLinter $linter) { $this->linter = $linter; + return $this; } /** * Statically evaluate a boolean value from an XHP tree. * * TODO: Improve this and move it to XHPAST proper? * * @param string The "semantic string" of a single value. * @return mixed `true` or `false` if the value could be evaluated * statically; `null` if static evaluation was not possible. */ protected function evaluateStaticBoolean($string) { switch (strtolower($string)) { case '0': case 'null': case 'false': return false; case '1': case 'true': return true; } return null; } protected function getConcreteVariableString(XHPASTNode $var) { $concrete = $var->getConcreteString(); // Strip off curly braces as in `$obj->{$property}`. $concrete = trim($concrete, '{}'); return $concrete; } // These methods are proxied to the @{class:ArcanistLinter}. final public function getActivePath() { return $this->linter->getActivePath(); } final public function getOtherLocation($offset, $path = null) { return $this->linter->getOtherLocation($offset, $path); } final protected function raiseLintAtNode( XHPASTNode $node, $desc, $replace = null) { return $this->linter->raiseLintAtNode( $node, $this->getLintID(), $desc, $replace); } final public function raiseLintAtOffset( $offset, $desc, $text = null, $replace = null) { return $this->linter->raiseLintAtOffset( $offset, $this->getLintID(), $desc, $text, $replace); } final protected function raiseLintAtToken( XHPASTToken $token, $desc, $replace = null) { return $this->linter->raiseLintAtToken( $token, $this->getLintID(), $desc, $replace); } /* -( Utility )------------------------------------------------------------ */ /** * Retrieve all calls to some specified function(s). * * Returns all descendant nodes which represent a function call to one of the * specified functions. * * @param XHPASTNode Root node. * @param list Function names. * @return AASTNodeList */ protected function getFunctionCalls(XHPASTNode $root, array $function_names) { $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); $nodes = array(); foreach ($calls as $call) { $node = $call->getChildByIndex(0); $name = strtolower($node->getConcreteString()); if (in_array($name, $function_names)) { $nodes[] = $call; } } return AASTNodeList::newFromTreeAndNodes($root->getTree(), $nodes); } public function getSuperGlobalNames() { return array( '$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV', ); } } diff --git a/src/unit/engine/NoseTestEngine.php b/src/unit/engine/NoseTestEngine.php index 9af83c56..5e5870e1 100644 --- a/src/unit/engine/NoseTestEngine.php +++ b/src/unit/engine/NoseTestEngine.php @@ -1,166 +1,166 @@ getPaths(); $affected_tests = array(); foreach ($paths as $path) { $absolute_path = Filesystem::resolvePath($path); if (is_dir($absolute_path)) { $absolute_test_path = Filesystem::resolvePath('tests/'.$path); if (is_readable($absolute_test_path)) { $affected_tests[] = $absolute_test_path; } } if (is_readable($absolute_path)) { $filename = basename($path); $directory = dirname($path); // assumes directory layout: tests//test_.py $relative_test_path = 'tests/'.$directory.'/test_'.$filename; $absolute_test_path = Filesystem::resolvePath($relative_test_path); if (is_readable($absolute_test_path)) { $affected_tests[] = $absolute_test_path; } } } return $this->runTests($affected_tests, './'); } public function runTests($test_paths, $source_path) { if (empty($test_paths)) { return array(); } $futures = array(); $tmpfiles = array(); foreach ($test_paths as $test_path) { $xunit_tmp = new TempFile(); $cover_tmp = new TempFile(); $future = $this->buildTestFuture($test_path, $xunit_tmp, $cover_tmp); $futures[$test_path] = $future; $tmpfiles[$test_path] = array( 'xunit' => $xunit_tmp, 'cover' => $cover_tmp, ); } $results = array(); $futures = id(new FutureIterator($futures)) ->limit(4); foreach ($futures as $test_path => $future) { try { list($stdout, $stderr) = $future->resolvex(); - } catch(CommandException $exc) { + } catch (CommandException $exc) { if ($exc->getError() > 1) { // 'nose' returns 1 when tests are failing/broken. throw $exc; } } $xunit_tmp = $tmpfiles[$test_path]['xunit']; $cover_tmp = $tmpfiles[$test_path]['cover']; $this->parser = new ArcanistXUnitTestResultParser(); $results[] = $this->parseTestResults( $source_path, $xunit_tmp, $cover_tmp); } return array_mergev($results); } public function buildTestFuture($path, $xunit_tmp, $cover_tmp) { $cmd_line = csprintf( 'nosetests --with-xunit --xunit-file=%s', $xunit_tmp); if ($this->getEnableCoverage() !== false) { $cmd_line .= csprintf( ' --with-coverage --cover-xml --cover-xml-file=%s', $cover_tmp); } return new ExecFuture('%C %s', $cmd_line, $path); } public function parseTestResults($source_path, $xunit_tmp, $cover_tmp) { $results = $this->parser->parseTestResults( Filesystem::readFile($xunit_tmp)); // coverage is for all testcases in the executed $path if ($this->getEnableCoverage() !== false) { $coverage = $this->readCoverage($cover_tmp, $source_path); foreach ($results as $result) { $result->setCoverage($coverage); } } return $results; } public function readCoverage($cover_file, $source_path) { $coverage_dom = new DOMDocument(); $coverage_dom->loadXML(Filesystem::readFile($cover_file)); $reports = array(); $classes = $coverage_dom->getElementsByTagName('class'); foreach ($classes as $class) { $path = $class->getAttribute('filename'); $root = $this->getWorkingCopy()->getProjectRoot(); if (!Filesystem::isDescendant($path, $root)) { continue; } // get total line count in file $line_count = count(phutil_split_lines(Filesystem::readFile($path))); $coverage = ''; $start_line = 1; $lines = $class->getElementsByTagName('line'); for ($ii = 0; $ii < $lines->length; $ii++) { $line = $lines->item($ii); $next_line = intval($line->getAttribute('number')); for ($start_line; $start_line < $next_line; $start_line++) { $coverage .= 'N'; } if (intval($line->getAttribute('hits')) == 0) { $coverage .= 'U'; } else if (intval($line->getAttribute('hits')) > 0) { $coverage .= 'C'; } $start_line++; } if ($start_line < $line_count) { foreach (range($start_line, $line_count) as $line_num) { $coverage .= 'N'; } } $reports[$path] = $coverage; } return $reports; } } diff --git a/src/unit/engine/XUnitTestEngine.php b/src/unit/engine/XUnitTestEngine.php index d97c6f0e..803880af 100644 --- a/src/unit/engine/XUnitTestEngine.php +++ b/src/unit/engine/XUnitTestEngine.php @@ -1,465 +1,465 @@ projectRoot = $this->getWorkingCopy()->getProjectRoot(); // Determine build engine. if (Filesystem::binaryExists('msbuild')) { $this->buildEngine = 'msbuild'; } else if (Filesystem::binaryExists('xbuild')) { $this->buildEngine = 'xbuild'; } else { throw new Exception( pht( 'Unable to find %s or %s in %s!', 'msbuild', 'xbuild', 'PATH')); } // Determine runtime engine (.NET or Mono). if (phutil_is_windows()) { $this->runtimeEngine = ''; } else if (Filesystem::binaryExists('mono')) { $this->runtimeEngine = Filesystem::resolveBinary('mono'); } else { throw new Exception( pht('Unable to find Mono and you are not on Windows!')); } // Read the discovery rules. $this->discoveryRules = $this->getConfigurationManager()->getConfigFromAnySource( 'unit.csharp.discovery'); if ($this->discoveryRules === null) { throw new Exception( pht( 'You must configure discovery rules to map C# files '. 'back to test projects (`%s` in %s).', 'unit.csharp.discovery', '.arcconfig')); } // Determine xUnit test runner path. if ($this->xunitHintPath === null) { $this->xunitHintPath = $this->getConfigurationManager()->getConfigFromAnySource( 'unit.csharp.xunit.binary'); } $xunit = $this->projectRoot.DIRECTORY_SEPARATOR.$this->xunitHintPath; if (file_exists($xunit) && $this->xunitHintPath !== null) { $this->testEngine = Filesystem::resolvePath($xunit); } else if (Filesystem::binaryExists('xunit.console.clr4.exe')) { $this->testEngine = 'xunit.console.clr4.exe'; } else { throw new Exception( pht( "Unable to locate xUnit console runner. Configure ". "it with the `%s' option in %s.", 'unit.csharp.xunit.binary', '.arcconfig')); } } /** * Main entry point for the test engine. Determines what assemblies to build * and test based on the files that have changed. * * @return array Array of test results. */ public function run() { $this->loadEnvironment(); if ($this->getRunAllTests()) { $paths = id(new FileFinder($this->projectRoot))->find(); } else { $paths = $this->getPaths(); } return $this->runAllTests($this->mapPathsToResults($paths)); } /** * Applies the discovery rules to the set of paths specified. * * @param array Array of paths. * @return array Array of paths to test projects and assemblies. */ public function mapPathsToResults(array $paths) { $results = array(); foreach ($this->discoveryRules as $regex => $targets) { $regex = str_replace('/', '\\/', $regex); foreach ($paths as $path) { if (preg_match('/'.$regex.'/', $path) === 1) { foreach ($targets as $target) { // Index 0 is the test project (.csproj file) // Index 1 is the output assembly (.dll file) $project = preg_replace('/'.$regex.'/', $target[0], $path); $project = $this->projectRoot.DIRECTORY_SEPARATOR.$project; $assembly = preg_replace('/'.$regex.'/', $target[1], $path); $assembly = $this->projectRoot.DIRECTORY_SEPARATOR.$assembly; if (file_exists($project)) { $project = Filesystem::resolvePath($project); $assembly = Filesystem::resolvePath($assembly); // Check to ensure uniqueness. $exists = false; foreach ($results as $existing) { if ($existing['assembly'] === $assembly) { $exists = true; break; } } if (!$exists) { $results[] = array( 'project' => $project, 'assembly' => $assembly, ); } } } } } } return $results; } /** * Builds and runs the specified test assemblies. * * @param array Array of paths to test project files. * @return array Array of test results. */ public function runAllTests(array $test_projects) { if (empty($test_projects)) { return array(); } $results = array(); $results[] = $this->generateProjects(); if ($this->resultsContainFailures($results)) { return array_mergev($results); } $results[] = $this->buildProjects($test_projects); if ($this->resultsContainFailures($results)) { return array_mergev($results); } $results[] = $this->testAssemblies($test_projects); return array_mergev($results); } /** * Determine whether or not a current set of results contains any failures. * This is needed since we build the assemblies as part of the unit tests, but * we can't run any of the unit tests if the build fails. * * @param array Array of results to check. * @return bool If there are any failures in the results. */ private function resultsContainFailures(array $results) { $results = array_mergev($results); foreach ($results as $result) { if ($result->getResult() != ArcanistUnitTestResult::RESULT_PASS) { return true; } } return false; } /** * If the `Build` directory exists, we assume that this is a multi-platform * project that requires generation of C# project files. Because we want to * test that the generation and subsequent build is whole, we need to * regenerate any projects in case the developer has added files through an * IDE and then forgotten to add them to the respective `.definitions` file. * By regenerating the projects we ensure that any missing definition entries * will cause the build to fail. * * @return array Array of test results. */ private function generateProjects() { // No "Build" directory; so skip generation of projects. if (!is_dir(Filesystem::resolvePath($this->projectRoot.'/Build'))) { return array(); } // No "Protobuild.exe" file; so skip generation of projects. if (!is_file(Filesystem::resolvePath( $this->projectRoot.'/Protobuild.exe'))) { return array(); } // Work out what platform the user is building for already. $platform = phutil_is_windows() ? 'Windows' : 'Linux'; $files = Filesystem::listDirectory($this->projectRoot); foreach ($files as $file) { if (strtolower(substr($file, -4)) == '.sln') { $parts = explode('.', $file); $platform = $parts[count($parts) - 2]; break; } } $regenerate_start = microtime(true); $regenerate_future = new ExecFuture( '%C Protobuild.exe --resync %s', $this->runtimeEngine, $platform); $regenerate_future->setCWD(Filesystem::resolvePath( $this->projectRoot)); $results = array(); $result = new ArcanistUnitTestResult(); $result->setName(pht('(regenerate projects for %s)', $platform)); try { $regenerate_future->resolvex(); $result->setResult(ArcanistUnitTestResult::RESULT_PASS); - } catch(CommandException $exc) { + } catch (CommandException $exc) { if ($exc->getError() > 1) { throw $exc; } $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); $result->setUserdata($exc->getStdout()); } $result->setDuration(microtime(true) - $regenerate_start); $results[] = $result; return $results; } /** * Build the projects relevant for the specified test assemblies and return * the results of the builds as test results. This build also passes the * "SkipTestsOnBuild" parameter when building the projects, so that MSBuild * conditionals can be used to prevent any tests running as part of the * build itself (since the unit tester is about to run each of the tests * individually). * * @param array Array of test assemblies. * @return array Array of test results. */ private function buildProjects(array $test_assemblies) { $build_futures = array(); $build_failed = false; $build_start = microtime(true); $results = array(); foreach ($test_assemblies as $test_assembly) { $build_future = new ExecFuture( '%C %s', $this->buildEngine, '/p:SkipTestsOnBuild=True'); $build_future->setCWD(Filesystem::resolvePath( dirname($test_assembly['project']))); $build_futures[$test_assembly['project']] = $build_future; } $iterator = id(new FutureIterator($build_futures))->limit(1); foreach ($iterator as $test_assembly => $future) { $result = new ArcanistUnitTestResult(); $result->setName('(build) '.$test_assembly); try { $future->resolvex(); $result->setResult(ArcanistUnitTestResult::RESULT_PASS); - } catch(CommandException $exc) { + } catch (CommandException $exc) { if ($exc->getError() > 1) { throw $exc; } $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); $result->setUserdata($exc->getStdout()); $build_failed = true; } $result->setDuration(microtime(true) - $build_start); $results[] = $result; } return $results; } /** * Build the future for running a unit test. This can be overridden to enable * support for code coverage via another tool. * * @param string Name of the test assembly. * @return array The future, output filename and coverage filename * stored in an array. */ protected function buildTestFuture($test_assembly) { // FIXME: Can't use TempFile here as xUnit doesn't like // UNIX-style full paths. It sees the leading / as the // start of an option flag, even when quoted. $xunit_temp = Filesystem::readRandomCharacters(10).'.results.xml'; if (file_exists($xunit_temp)) { unlink($xunit_temp); } $future = new ExecFuture( '%C %s /xml %s', trim($this->runtimeEngine.' '.$this->testEngine), $test_assembly, $xunit_temp); $folder = Filesystem::resolvePath($this->projectRoot); $future->setCWD($folder); $combined = $folder.'/'.$xunit_temp; if (phutil_is_windows()) { $combined = $folder.'\\'.$xunit_temp; } return array($future, $combined, null); } /** * Run the xUnit test runner on each of the assemblies and parse the * resulting XML. * * @param array Array of test assemblies. * @return array Array of test results. */ private function testAssemblies(array $test_assemblies) { $results = array(); // Build the futures for running the tests. $futures = array(); $outputs = array(); $coverages = array(); foreach ($test_assemblies as $test_assembly) { list($future_r, $xunit_temp, $coverage) = $this->buildTestFuture($test_assembly['assembly']); $futures[$test_assembly['assembly']] = $future_r; $outputs[$test_assembly['assembly']] = $xunit_temp; $coverages[$test_assembly['assembly']] = $coverage; } // Run all of the tests. $futures = id(new FutureIterator($futures)) ->limit(8); foreach ($futures as $test_assembly => $future) { list($err, $stdout, $stderr) = $future->resolve(); if (file_exists($outputs[$test_assembly])) { $result = $this->parseTestResult( $outputs[$test_assembly], $coverages[$test_assembly]); $results[] = $result; unlink($outputs[$test_assembly]); } else { // FIXME: There's a bug in Mono which causes a segmentation fault // when xUnit.NET runs; this causes the XML file to not appear // (depending on when the segmentation fault occurs). See // https://bugzilla.xamarin.com/show_bug.cgi?id=16379 // for more information. // Since it's not possible for the user to correct this error, we // ignore the fact the tests didn't run here. } } return array_mergev($results); } /** * Returns null for this implementation as xUnit does not support code * coverage directly. Override this method in another class to provide code * coverage information (also see @{class:CSharpToolsUnitEngine}). * * @param string The name of the coverage file if one was provided by * `buildTestFuture`. * @return array Code coverage results, or null. */ protected function parseCoverageResult($coverage) { return null; } /** * Parses the test results from xUnit. * * @param string The name of the xUnit results file. * @param string The name of the coverage file if one was provided by * `buildTestFuture`. This is passed through to * `parseCoverageResult`. * @return array Test results. */ private function parseTestResult($xunit_tmp, $coverage) { $xunit_dom = new DOMDocument(); $xunit_dom->loadXML(Filesystem::readFile($xunit_tmp)); $results = array(); $tests = $xunit_dom->getElementsByTagName('test'); foreach ($tests as $test) { $name = $test->getAttribute('name'); $time = $test->getAttribute('time'); $status = ArcanistUnitTestResult::RESULT_UNSOUND; switch ($test->getAttribute('result')) { case 'Pass': $status = ArcanistUnitTestResult::RESULT_PASS; break; case 'Fail': $status = ArcanistUnitTestResult::RESULT_FAIL; break; case 'Skip': $status = ArcanistUnitTestResult::RESULT_SKIP; break; } $userdata = ''; $reason = $test->getElementsByTagName('reason'); $failure = $test->getElementsByTagName('failure'); if ($reason->length > 0 || $failure->length > 0) { $node = ($reason->length > 0) ? $reason : $failure; $message = $node->item(0)->getElementsByTagName('message'); if ($message->length > 0) { $userdata = $message->item(0)->nodeValue; } $stacktrace = $node->item(0)->getElementsByTagName('stack-trace'); if ($stacktrace->length > 0) { $userdata .= "\n".$stacktrace->item(0)->nodeValue; } } $result = new ArcanistUnitTestResult(); $result->setName($name); $result->setResult($status); $result->setDuration($time); $result->setUserData($userdata); if ($coverage != null) { $result->setCoverage($this->parseCoverageResult($coverage)); } $results[] = $result; } return $results; } }