diff --git a/scripts/arcanist.php b/scripts/arcanist.php index 5091c897..b838da1d 100755 --- a/scripts/arcanist.php +++ b/scripts/arcanist.php @@ -1,181 +1,181 @@ #!/usr/bin/env php $arg) { if ($arg == '--') { break; } else if ($arg == '--trace') { unset($args[$key]); $config_trace_mode = true; } } $args = array_values($args); try { if ($config_trace_mode) { ExecFuture::pushEchoMode(true); } if (!$args) { throw new ArcanistUsageException("No command provided. Try 'arc help'."); } $working_copy = ArcanistWorkingCopyIdentity::newFromPath($_SERVER['PWD']); $libs = $working_copy->getConfig('phutil_libraries'); if ($libs) { foreach ($libs as $name => $location) { if ($config_trace_mode) { echo "Loading phutil library '{$name}' from '{$location}'...\n"; } $library_root = Filesystem::resolvePath( $location, $working_copy->getProjectRoot()); phutil_load_library($library_root); } } $config = $working_copy->getConfig('arcanist_configuration'); if ($config) { - phutil_autoload_class($config); + PhutilSymbolLoader::loadClass($config); $config = new $config(); } else { $config = new ArcanistConfiguration(); } $command = strtolower($args[0]); $workflow = $config->buildWorkflow($command); if (!$workflow) { throw new ArcanistUsageException( "Unknown command '{$command}'. Try 'arc help'."); } $workflow->setArcanistConfiguration($config); $workflow->setCommand($command); $workflow->parseArguments(array_slice($args, 1)); $need_working_copy = $workflow->requiresWorkingCopy(); $need_conduit = $workflow->requiresConduit(); $need_auth = $workflow->requiresAuthentication(); $need_repository_api = $workflow->requiresRepositoryAPI(); $need_conduit = $need_conduit || $need_auth; $need_working_copy = $need_working_copy || $need_conduit || $need_repository_api; if ($need_working_copy) { $workflow->setWorkingCopy($working_copy); } if ($need_conduit) { $conduit_uri = $working_copy->getConduitURI(); if (!$conduit_uri) { throw new ArcanistUsageException( "No Conduit URI is specified in the .arcconfig file for this project. ". "Specify the Conduit URI for the host Differential is running on."); } $conduit = new ConduitClient($conduit_uri); $conduit->setTraceMode($config_trace_mode); $workflow->setConduit($conduit); $description = implode(' ', $argv); $connection = $conduit->callMethodSynchronous( 'conduit.connect', array( 'client' => 'arc', 'clientVersion' => 2, 'clientDescription' => php_uname('n').':'.$description, 'user' => getenv('USER'), )); $conduit->setConnectionID($connection['connectionID']); } if ($need_repository_api) { $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity( $working_copy); $workflow->setRepositoryAPI($repository_api); } if ($need_auth) { $user_name = getenv('USER'); $user_find_future = $conduit->callMethod( 'user.find', array( 'aliases' => array( $user_name, ), )); $user_guids = $user_find_future->resolve(); if (empty($user_guids[$user_name])) { throw new ArcanistUsageException( "Username '{$user_name}' is not recognized."); } $user_guid = $user_guids[$user_name]; $workflow->setUserGUID($user_guid); $workflow->setUserName($user_name); } $config->willRunWorkflow($command, $workflow); $workflow->willRunWorkflow(); $err = $workflow->run(); if ($err == 0) { $config->didRunWorkflow($command, $workflow); } exit($err); } catch (ArcanistUsageException $ex) { echo phutil_console_format( "**Usage Exception:** %s\n", $ex->getMessage()); if ($config_trace_mode) { echo "\n"; throw $ex; } exit(1); } catch (Exception $ex) { if ($config_trace_mode) { throw $ex; } echo phutil_console_format( "\n**Exception:**\n%s\n%s\n", $ex->getMessage(), "(Run with --trace for a full exception trace.)"); exit(1); } diff --git a/scripts/phutil_analyzer.php b/scripts/phutil_analyzer.php index 67b679d7..a31b8d37 100755 --- a/scripts/phutil_analyzer.php +++ b/scripts/phutil_analyzer.php @@ -1,366 +1,366 @@ #!/usr/bin/env php array_fill_keys($builtin_classes, true), + 'class' => array_fill_keys($builtin_classes, true) + array( + 'PhutilBootloader' => true, + ), 'function' => array_fill_keys($builtin_functions, true) + array( 'empty' => true, 'isset' => true, 'echo' => true, 'print' => true, 'exit' => true, 'die' => true, - - 'phutil_module_exists' => true, ), 'interface' => array_fill_keys($builtin_interfaces, true), ); require_once dirname(__FILE__).'/__init_script__.php'; if ($argc != 2) { $self = basename($argv[0]); echo "usage: {$self} \n"; exit(1); } phutil_require_module('phutil', 'filesystem'); $dir = Filesystem::resolvePath($argv[1]); phutil_require_module('phutil', 'parser/xhpast/bin'); phutil_require_module('phutil', 'parser/xhpast/api/tree'); phutil_require_module('arcanist', 'lint/linter/phutilmodule'); phutil_require_module('arcanist', 'lint/message'); phutil_require_module('arcanist', 'staticanalysis/parsers/phutilmodule'); $data = array(); $futures = array(); foreach (Filesystem::listDirectory($dir, $hidden_files = false) as $file) { if (!preg_match('/.php$/', $file)) { continue; } $data[$file] = Filesystem::readFile($dir.'/'.$file); $futures[$file] = xhpast_get_parser_future($data[$file]); } $requirements = new PhutilModuleRequirements(); $requirements->addBuiltins($builtin); $has_init = false; $has_files = false; foreach (Futures($futures) as $file => $future) { try { $tree = XHPASTTree::newFromDataAndResolvedExecFuture( $data[$file], $future->resolve()); } catch (XHPASTSyntaxErrorException $ex) { echo "Syntax Error! In '{$file}': ".$ex->getMessage()."\n"; exit(1); } $root = $tree->getRootNode(); $requirements->setCurrentFile($file); if ($file == '__init__.php') { $has_init = true; $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $name = $call->getChildByIndex(0); $call_name = $name->getConcreteString(); if ($call_name == 'phutil_require_source') { $params = $call->getChildByIndex(1)->getChildren(); if (count($params) !== 1) { $requirements->addLint( $call, $call->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, "Call to phutil_require_source() must have exactly one argument."); continue; } $param = reset($params); $value = $param->getStringLiteralValue(); if ($value === null) { $requirements->addLint( $param, $param->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, "phutil_require_source() parameter must be a string literal."); continue; } $requirements->addSourceDependency($name, $value); } else if ($call_name == 'phutil_require_module') { analyze_require_module($call, $requirements); } } } else { $has_files = true; $requirements->addSourceDeclaration(basename($file)); // Function uses: // - Explicit call // TODO?: String literal in ReflectionFunction(). $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $name = $call->getChildByIndex(0); if ($name->getTypeName() == 'n_VARIABLE' || $name->getTypeName() == 'n_VARIABLE_VARIABLE') { $requirements->addLint( $name, $name->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC, "Use of variable function calls prevents dependencies from being ". "checked statically. This module may have undetectable errors."); continue; } if ($name->getTypeName() == 'n_CLASS_STATIC_ACCESS') { // We'll pick this up later. continue; } $call_name = $name->getConcreteString(); if ($call_name == 'phutil_require_module') { analyze_require_module($call, $requirements); } else if ($call_name == 'call_user_func' || $call_name == 'call_user_func_array') { $params = $call->getChildByIndex(1)->getChildren(); if (count($params) == 0) { $requirements->addLint( $call, $call->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, "Call to {$call_name}() must have at least one argument."); } $symbol = array_shift($params); $symbol_value = $symbol->getStringLiteralValue(); if ($symbol_value) { $requirements->addFunctionDependency( $symbol, $symbol_value); } else { $requirements->addLint( $symbol, $symbol->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC, "Use of variable arguments to {$call_name} prevents dependencies ". "from being checked statically. This module may have undetectable ". "errors."); } } else { $requirements->addFunctionDependency( $name, $name->getConcreteString()); } } $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($functions as $function) { $name = $function->getChildByIndex(2); $requirements->addFunctionDeclaration( $name, $name->getConcreteString()); } // Class uses: // - new // - extends (in class declaration) // - Static method call // - Static property access // - Constant use // TODO?: String literal in ReflectionClass(). // TODO?: String literal in array literal in call_user_func / // call_user_func_array(). $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1); $requirements->addClassDeclaration( $class_name, $class_name->getConcreteString()); $extends = $class->getChildByIndex(2); foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { $requirements->addClassDependency( $class_name->getConcreteString(), $parent, $parent->getConcreteString()); } $implements = $class->getChildByIndex(3); $interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME'); foreach ($interfaces as $interface) { $requirements->addInterfaceDependency( $class_name->getConcreteString(), $interface, $interface->getConcreteString()); } } if (count($classes) > 1) { foreach ($classes as $class) { $class_name = $class->getChildByIndex(1); $class_string = $class_name->getConcreteString(); $requirements->addLint( $class_name, $class_string, ArcanistPhutilModuleLinter::LINT_ANALYZER_MULTIPLE_CLASSES, "This file declares more than one class. Declare only one class per ". "file."); break; } } else if (count($classes) == 1) { foreach ($classes as $class) { $class_name = $class->getChildByIndex(1); $class_string = $class_name->getConcreteString(); if ($file != $class_string.'.php') { $rename = $class_string.'.php'; $requirements->addLint( $class_name, $class_string, ArcanistPhutilModuleLinter::LINT_ANALYZER_CLASS_FILENAME, "The name of this file differs from the name of the class it ". "declares. Rename the file to '{$rename}'."); } break; } } $uses_of_new = $root->selectDescendantsOfType('n_NEW'); foreach ($uses_of_new as $new_operator) { $name = $new_operator->getChildByIndex(0); if ($name->getTypeName() == 'n_VARIABLE' || $name->getTypeName() == 'n_VARIABLE_VARIABLE') { $requirements->addLint( $name, $name->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC, "Use of variable class instantiation prevents dependencies from ". "being checked statically. This module may have undetectable ". "errors."); continue; } $requirements->addClassDependency( null, $name, $name->getConcreteString()); } $static_uses = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); foreach ($static_uses as $static_use) { $name = $static_use->getChildByIndex(0); if ($name->getTypeName() != 'n_CLASS_NAME') { echo "WARNING UNLINTABLE\n"; continue; } $name_concrete = $name->getConcreteString(); $magic_names = array( 'static' => true, 'parent' => true, 'self' => true, ); if (isset($magic_names[$name_concrete])) { continue; } $requirements->addClassDependency( null, $name, $name_concrete); } // Interface uses: // - implements // - extends (in interface declaration) $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($interfaces as $interface) { $interface_name = $interface->getChildByIndex(1); $requirements->addInterfaceDeclaration( $interface_name, $interface_name->getConcreteString()); $extends = $interface->getChildByIndex(2); foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { $requirements->addInterfaceDependency( $class_name->getConcreteString(), $parent, $parent->getConcreteString()); } } } } if (!$has_init && $has_files) { $requirements->addRawLint( ArcanistPhutilModuleLinter::LINT_ANALYZER_NO_INIT, "Create an __init__.php file in this module."); } echo json_encode($requirements->toDictionary()); function analyze_require_module( XHPASTNode $call, PhutilModuleRequirements $requirements) { $name = $call->getChildByIndex(0); $params = $call->getChildByIndex(1)->getChildren(); if (count($params) !== 2) { $requirements->addLint( $call, $call->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, "Call to phutil_require_module() must have exactly two arguments."); return; } $module_param = array_pop($params); $library_param = array_pop($params); $library_value = $library_param->getStringLiteralValue(); if ($library_value === null) { $requirements->addLint( $library_param, $library_param->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, "phutil_require_module() parameters must be string literals."); return; } $module_value = $module_param->getStringLiteralValue(); if ($module_value === null) { $requirements->addLint( $module_param, $module_param->getConcreteString(), ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, "phutil_require_module() parameters must be string literals."); return; } $requirements->addModuleDependency( $name, $library_value.':'.$module_value); } diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php index 17a4d4fe..e8659267 100644 --- a/src/configuration/ArcanistConfiguration.php +++ b/src/configuration/ArcanistConfiguration.php @@ -1,80 +1,88 @@ setType('class') + ->setName($workflow_class) + ->setLibrary('arcanist') + ->selectAndLoadSymbols(); + + if (!$symbols) { + return null; + } return newv($workflow_class, array()); } public function buildAllWorkflows() { - $classes = phutil_find_class_descendants('ArcanistBaseWorkflow'); + $symbols = id(new PhutilSymbolLoader()) + ->setType('class') + ->setAncestorClass('ArcanistBaseWorkflow') + ->setLibrary('arcanist') + ->selectAndLoadSymbols(); + $workflows = array(); - foreach ($classes as $class) { + foreach ($symbols as $symbol) { + $class = $class['name']; $name = preg_replace('/^Arcanist(\w+)Workflow$/', '\1', $class); $name = strtolower($name); - phutil_autoload_class($class); $workflows[$name] = newv($class, array()); } return $workflows; } public function willRunWorkflow($command, ArcanistBaseWorkflow $workflow) { // This is a hook. } public function didRunWorkflow($command, ArcanistBaseWorkflow $workflow) { // This is a hook. } public function getCustomArgumentsForCommand($command) { return array(); } public static function replaceClassnameHyphens($m) { return strtoupper($m[1]); } } diff --git a/src/configuration/__init__.php b/src/configuration/__init__.php index 7c8a7b8d..27e15bce 100644 --- a/src/configuration/__init__.php +++ b/src/configuration/__init__.php @@ -1,13 +1,13 @@ 'Use of Undeclared Class', self::LINT_UNDECLARED_FUNCTION => 'Use of Undeclared Function', self::LINT_UNDECLARED_INTERFACE => 'Use of Undeclared Interface', self::LINT_UNDECLARED_SOURCE => 'Use of Nonexistent File', self::LINT_UNUSED_SOURCE => 'Unused Source', self::LINT_UNUSED_MODULE => 'Unused Module', self::LINT_INIT_REBUILD => 'Rebuilt __init__.php File', self::LINT_UNKNOWN_CLASS => 'Unknown Class', self::LINT_UNKNOWN_FUNCTION => 'Unknown Function', self::LINT_ANALYZER_SIGNATURE => 'Analyzer: Bad Call Signature', self::LINT_ANALYZER_DYNAMIC => 'Analyzer: Dynamic Dependency', self::LINT_ANALYZER_NO_INIT => 'Analyzer: No __init__.php File', self::LINT_ANALYZER_MULTIPLE_CLASSES => 'Analyzer: File Declares Multiple Classes', self::LINT_ANALYZER_CLASS_FILENAME => 'Analyzer: Filename Does Not Match Class Declaration', ); } public function getLinterName() { return 'PHU'; } public function getLintSeverityMap() { return array( self::LINT_ANALYZER_DYNAMIC => ArcanistLintSeverity::SEVERITY_WARNING, ); } private $moduleInfo = array(); private $unknownClasses = array(); private $unknownFunctions = array(); private function setModuleInfo($key, array $info) { $this->moduleInfo[$key] = $info; } private function getModulePathOnDisk($key) { $info = $this->moduleInfo[$key]; return $info['root'].'/'.$info['module']; } private function getModuleDisplayName($key) { $info = $this->moduleInfo[$key]; return $info['module']; } private function isPhutilLibraryMetadata($path) { $file = basename($path); return !strncmp('__phutil_library_', $file, strlen('__phutil_library_')); } public function willLintPaths(array $paths) { if ($paths) { if (!xhpast_is_available()) { throw new Exception(xhpast_get_build_instructions()); } } $modules = array(); $moduleinfo = array(); $project_root = $this->getEngine()->getWorkingCopy()->getProjectRoot(); foreach ($paths as $path) { $absolute_path = $project_root.'/'.$path; $library_root = phutil_get_library_root_for_path($absolute_path); if (!$library_root) { continue; } if ($this->isPhutilLibraryMetadata($path)) { continue; } $library_name = phutil_get_library_name_for_root($library_root); if (!is_dir($path)) { $path = dirname($path); } $path = Filesystem::resolvePath( $path, $project_root); if ($path == $library_root) { continue; } $module_name = Filesystem::readablePath($path, $library_root); $module_key = $library_name.':'.$module_name; if (empty($modules[$module_key])) { $modules[$module_key] = $module_key; $this->setModuleInfo($module_key, array( 'library' => $library_name, 'root' => $library_root, 'module' => $module_name, )); } } if (!$modules) { return; } $modules = array_keys($modules); $arc_root = phutil_get_library_root('arcanist'); $bin = dirname($arc_root).'/scripts/phutil_analyzer.php'; $futures = array(); foreach ($modules as $mkey => $key) { $disk_path = $this->getModulePathOnDisk($key); if (Filesystem::pathExists($disk_path)) { $futures[$key] = new ExecFuture( '%s %s', $bin, $disk_path); } else { // This can occur in git when you add a module in HEAD and then remove // it in unstaged changes in the working copy. Just ignore it. unset($modules[$mkey]); } } $requirements = array(); foreach (Futures($futures) as $key => $future) { $requirements[$key] = $future->resolveJSON(); } $dependencies = array(); $futures = array(); foreach ($requirements as $key => $requirement) { foreach ($requirement['messages'] as $message) { list($where, $text, $code, $description) = $message; if ($where) { $where = array($where); } $this->raiseLintInModule( $key, $code, $description, $where, $text); } foreach ($requirement['requires']['module'] as $req_module => $where) { if (isset($requirements[$req_module])) { $dependencies[$req_module] = $requirements[$req_module]; } else { list($library_name, $module_name) = explode(':', $req_module); $library_root = phutil_get_library_root($library_name); $this->setModuleInfo($req_module, array( 'library' => $library_name, 'root' => $library_root, 'module' => $module_name, )); $disk_path = $this->getModulePathOnDisk($req_module); if (Filesystem::pathExists($disk_path)) { $futures[$req_module] = new ExecFuture( '%s %s', $bin, $disk_path); } else { $dependencies[$req_module] = array(); } } } } foreach (Futures($futures) as $key => $future) { $dependencies[$key] = $future->resolveJSON(); } foreach ($requirements as $key => $spec) { $deps = array_intersect_key( $dependencies, $spec['requires']['module']); $this->lintModule($key, $spec, $deps); } } private function lintModule($key, $spec, $deps) { $resolvable = array(); $need_classes = array(); $need_functions = array(); $drop_modules = array(); $used = array(); static $types = array( 'class' => self::LINT_UNDECLARED_CLASS, 'interface' => self::LINT_UNDECLARED_INTERFACE, 'function' => self::LINT_UNDECLARED_FUNCTION, ); foreach ($types as $type => $lint_code) { foreach ($spec['requires'][$type] as $name => $places) { $declared = $this->checkDependency( $type, $name, $deps); if (!$declared) { $module = $this->getModuleDisplayName($key); $message = $this->raiseLintInModule( $key, $lint_code, "Module '{$module}' uses {$type} '{$name}' but does not include ". "any module which declares it.", $places); if ($type == 'class' || $type == 'interface') { - $class_spec = PhutilLibraryMapRegistry::findClass( - $library = null, - $name); - if ($class_spec) { + $loader = new PhutilSymbolLoader(); + $loader->setType($type); + $loader->setName($name); + $symbols = $loader->selectSymbolsWithoutLoading(); + if ($symbols) { + $class_spec = reset($symbols); try { - $loaded = phutil_autoload_class($name); - } catch (PhutilLibraryLoadException $ex) { + $loader->selectAndLoadSymbols(); + $loaded = true; + } catch (PhutilMissingSymbolException $ex) { $loaded = false; } if ($loaded) { $resolvable[] = $message; $need_classes[$name] = $class_spec; } else { if (empty($this->unknownClasses[$name])) { $this->unknownClasses[$name] = true; $library = $class_spec['library']; $this->raiseLintInModule( $key, self::LINT_UNKNOWN_CLASS, "Class '{$name}' exists in the library map for library ". "'{$library}', but could not be loaded. You may need to ". "rebuild the library map.", $places); } } } else { if (empty($this->unknownClasses[$name])) { $this->unknownClasses[$name] = true; $this->raiseLintInModule( $key, self::LINT_UNKNOWN_CLASS, "Class '{$name}' could not be found in any known library. ". "You may need to rebuild the map for the library which ". "contains it.", $places); } } } else { - $func_spec = PhutilLibraryMapRegistry::findFunction( - $library = null, - $name); - if ($func_spec) { + $loader = new PhutilSymbolLoader(); + $loader->setType($type); + $loader->setName($name); + $symbols = $loader->selectSymbolsWithoutLoading(); + if ($symbols) { + $func_spec = reset($symbols); try { - $loaded = phutil_autoload_function($name); - } catch (PhutilLibraryLoadException $ex) { + $loader->selectAndLoadSymbols(); + $loaded = true; + } catch (PhutilMissingSymbolException $ex) { $loaded = false; } if ($loaded) { $resolvable[] = $message; $need_functions[$name] = $func_spec; } else { if (empty($this->unknownFunctions[$name])) { $this->unknownFunctions[$name] = true; $library = $func_spec['library']; $this->raiseLintInModule( $key, self::LINT_UNKNOWN_FUNCTION, "Function '{$name}' exists in the library map for library ". "'{$library}', but could not be loaded. You may need to ". "rebuild the library map.", $places); } } } else { if (empty($this->unknownFunctions[$name])) { $this->unknownFunctions[$name] = true; $this->raiseLintInModule( $key, self::LINT_UNKNOWN_FUNCTION, "Function '{$name}' could not be found in any known ". "library. You may need to rebuild the map for the library ". "which contains it.", $places); } } } } $used[$declared] = true; } } $unused = array_diff_key($deps, $used); foreach ($unused as $unused_module_key => $ignored) { $module = $this->getModuleDisplayName($key); $unused_module = $this->getModuleDisplayName($unused_module_key); $resolvable[] = $this->raiseLintInModule( $key, self::LINT_UNUSED_MODULE, "Module '{$module}' requires module '{$unused_module}' but does not ". "use anything it declares.", $spec['requires']['module'][$unused_module_key]); $drop_modules[] = $unused_module_key; } foreach ($spec['requires']['source'] as $file => $where) { if (empty($spec['declares']['source'][$file])) { $module = $this->getModuleDisplayName($key); $resolvable[] = $this->raiseLintInModule( $key, self::LINT_UNDECLARED_SOURCE, "Module '{$module}' requires source '{$file}', but it does not ". "exist.", $where); } } foreach ($spec['declares']['source'] as $file => $ignored) { if (empty($spec['requires']['source'][$file])) { $module = $this->getModuleDisplayName($key); $resolvable[] = $this->raiseLintInModule( $key, self::LINT_UNUSED_SOURCE, "Module '{$module}' does not include source file '{$file}'.", null); } } if ($resolvable) { $new_file = $this->buildNewModuleInit( $key, $spec, $need_classes, $need_functions, $drop_modules); $init_path = $this->getModulePathOnDisk($key).'/__init__.php'; $try_path = Filesystem::readablePath($init_path); if (Filesystem::pathExists($try_path)) { $init_path = $try_path; $old_file = Filesystem::readFile($init_path); } else { $old_file = ''; } $this->willLintPath($init_path); $message = $this->raiseLintAtOffset( null, self::LINT_INIT_REBUILD, "This generated phutil '__init__.php' file is suggested to address ". "lint problems with static dependencies in the module.", $old_file, $new_file); $message->setDependentMessages($resolvable); foreach ($resolvable as $message) { $message->setObsolete(true); } $message->setGenerateFile(true); } } private function buildNewModuleInit( $key, $spec, $need_classes, $need_functions, $drop_modules) { $init = array(); $init[] = ' $class_spec) { $modules[$class_spec['library'].':'.$class_spec['module']] = true; } foreach ($need_functions as $need => $func_spec) { $modules[$func_spec['library'].':'.$func_spec['module']] = true; } ksort($modules); $last = null; foreach ($modules as $module_key => $ignored) { if (is_array($ignored)) { $in_init = false; $in_file = false; foreach ($ignored as $where) { list($file, $line) = explode(':', $where); if ($file == '__init__.php') { $in_init = true; } else { $in_file = true; } } if ($in_file && !$in_init) { // If this is a runtime include, don't try to put it in the // __init__ file. continue; } } list($library, $module_name) = explode(':', $module_key); if ($last != $library) { $last = $library; if ($last != null) { $init[] = null; } } $library = "'".addcslashes($library, "'\\")."'"; $module_name = "'".addcslashes($module_name, "'\\")."'"; $init[] = "phutil_require_module({$library}, {$module_name});"; } $init[] = null; $init[] = null; $files = array_keys($spec['declares']['source']); sort($files); foreach ($files as $file) { $file = "'".addcslashes($file, "'\\")."'"; $init[] = "phutil_require_source({$file});"; } $init[] = null; return implode("\n", $init); } private function checkDependency($type, $name, $deps) { foreach ($deps as $key => $dep) { if (isset($dep['declares'][$type][$name])) { return $key; } } return false; } public function raiseLintInModule($key, $code, $desc, $places, $text = null) { if ($places) { foreach ($places as $place) { list($file, $offset) = explode(':', $place); $this->willLintPath( Filesystem::readablePath( $this->getModulePathOnDisk($key).'/'.$file, $this->getEngine()->getWorkingCopy()->getProjectRoot())); return $this->raiseLintAtOffset( $offset, $code, $desc, $text); } } else { $this->willLintPath($this->getModuleDisplayName($key)); return $this->raiseLintAtPath( $code, $desc); } } public function lintPath($path) { return; } } diff --git a/src/lint/linter/phutilmodule/__init__.php b/src/lint/linter/phutilmodule/__init__.php index 97c77273..28cd14ce 100644 --- a/src/lint/linter/phutilmodule/__init__.php +++ b/src/lint/linter/phutilmodule/__init__.php @@ -1,20 +1,20 @@ getSVNStatus() as $path => $mask) { if ($mask & self::FLAG_CONFLICT) { return true; } } return false; } public function getWorkingCopyStatus() { return $this->getSVNStatus(); } public function getSVNBaseRevisions() { if ($this->svnBaseRevisions === null) { $this->getSVNStatus(); } return $this->svnBaseRevisions; } public function getSVNStatus($with_externals = false) { if ($this->svnStatus === null) { list($status) = execx('(cd %s && svn --xml status)', $this->getPath()); $xml = new SimpleXMLElement($status); if (count($xml->target) != 1) { throw new Exception("Expected exactly one XML status target."); } $externals = array(); $files = array(); $target = $xml->target[0]; $this->svnBaseRevisions = array(); foreach ($target->entry as $entry) { $path = (string)$entry['path']; $mask = 0; $props = (string)($entry->{'wc-status'}[0]['props']); $item = (string)($entry->{'wc-status'}[0]['item']); $base = (string)($entry->{'wc-status'}[0]['revision']); $this->svnBaseRevisions[$path] = $base; switch ($props) { case 'none': case 'normal': break; case 'modified': $mask |= self::FLAG_MODIFIED; break; default: throw new Exception("Unrecognized property status '{$props}'."); } switch ($item) { case 'normal': break; case 'external': $mask |= self::FLAG_EXTERNALS; $externals[] = $path; break; case 'unversioned': $mask |= self::FLAG_UNTRACKED; break; case 'obstructed': $mask |= self::FLAG_OBSTRUCTED; break; case 'missing': $mask |= self::FLAG_MISSING; break; case 'added': $mask |= self::FLAG_ADDED; break; case 'modified': $mask |= self::FLAG_MODIFIED; break; case 'deleted': $mask |= self::FLAG_DELETED; break; default: throw new Exception("Unrecognized item status '{$item}'."); } $files[$path] = $mask; } foreach ($files as $path => $mask) { foreach ($externals as $external) { if (!strncmp($path, $external, strlen($external))) { $files[$path] |= self::FLAG_EXTERNALS; } } } $this->svnStatus = $files; } $status = $this->svnStatus; if (!$with_externals) { foreach ($status as $path => $mask) { if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) { unset($status[$path]); } } } return $status; } public function getSVNProperty($path, $property) { list($stdout) = execx( 'svn propget %s %s@', $property, $this->getPath($path)); return trim($stdout); } public function getSourceControlPath() { return idx($this->getSVNInfo('/'), 'URL'); } public function getSourceControlBaseRevision() { $info = $this->getSVNInfo('/'); return $info['URL'].'@'.$info['Revision']; } public function getBranchName() { return 'svn'; } public function buildInfoFuture($path) { if ($path == '/') { // When the root of a working copy is referenced by a symlink and you // execute 'svn info' on that symlink, svn fails. This is a longstanding // bug in svn: // // See http://subversion.tigris.org/issues/show_bug.cgi?id=2305 // // To reproduce, do: // // $ ln -s working_copy working_link - // $ svn info working_copy # ok + // $ svn info working_copy # ok // $ svn info working_link # fails // // Work around this by cd-ing into the directory before executing // 'svn info'. return new ExecFuture( '(cd %s && svn info .)', $this->getPath()); } else { // Note: here and elsewhere we need to append "@" to the path because if // a file has a literal "@" in it, everything after that will be // interpreted as a revision. By appending "@" with no argument, SVN // parses it properly. return new ExecFuture( 'svn info %s@', $this->getPath($path)); } } public function buildDiffFuture($path) { // The "--depth empty" flag prevents us from picking up changes in // children when we run 'diff' against a directory. return new ExecFuture( '(cd %s; svn diff --depth empty --diff-cmd diff -x -U%d %s)', $this->getPath(), $this->getDiffLinesOfContext(), $path); } public function primeSVNInfoResult($path, $result) { $this->svnInfoRaw[$path] = $result; return $this; } public function primeSVNDiffResult($path, $result) { $this->svnDiffRaw[$path] = $result; return $this; } public function getSVNInfo($path) { if (empty($this->svnInfo[$path])) { if (empty($this->svnInfoRaw[$path])) { $this->svnInfoRaw[$path] = $this->buildInfoFuture($path)->resolve(); } list($err, $stdout) = $this->svnInfoRaw[$path]; if ($err) { throw new Exception( "Error #{$err} executing svn info against '{$path}'."); } $patterns = array( '/^(URL): (\S+)$/m', '/^(Revision): (\d+)$/m', '/^(Last Changed Author): (\S+)$/m', '/^(Last Changed Rev): (\d+)$/m', '/^(Last Changed Date): (.+) \(.+\)$/m', '/^(Copied From URL): (\S+)$/m', '/^(Copied From Rev): (\d+)$/m', ); $result = array(); foreach ($patterns as $pattern) { $matches = null; if (preg_match($pattern, $stdout, $matches)) { $result[$matches[1]] = $matches[2]; } } if (isset($result['Last Changed Date'])) { $result['Last Changed Date'] = strtotime($result['Last Changed Date']); } if (empty($result)) { throw new Exception('Unable to parse SVN info.'); } $this->svnInfo[$path] = $result; } return $this->svnInfo[$path]; } public function getRawDiffText($path) { $status = $this->getSVNStatus(); if (!isset($status[$path])) { return null; } $status = $status[$path]; // Build meaningful diff text for "svn copy" operations. if ($status & ArcanistRepositoryAPI::FLAG_ADDED) { $info = $this->getSVNInfo($path); if (!empty($info['Copied From URL'])) { return $this->buildSyntheticAdditionDiff( $path, $info['Copied From URL'], $info['Copied From Rev']); } } // If we run "diff" on a binary file which doesn't have the "svn:mime-type" // of "application/octet-stream", `diff' will explode in a rain of // unhelpful hellfire as it tries to build a textual diff of the two // files. We just fix this inline since it's pretty unambiguous. // TODO: Move this to configuration? $matches = null; if (preg_match('/\.(gif|png|jpe?g|swf|pdf|ico)$/i', $path, $matches)) { $mime = $this->getSVNProperty($path, 'svn:mime-type'); if ($mime != 'application/octet-stream') { execx( 'svn propset svn:mime-type application/octet-stream %s', $this->getPath($path)); } } if (empty($this->svnDiffRaw[$path])) { $this->svnDiffRaw[$path] = $this->buildDiffFuture($path)->resolve(); } list($err, $stdout, $stderr) = $this->svnDiffRaw[$path]; // Note: GNU Diff returns 2 when SVN hands it binary files to diff and they // differ. This is not an error; it is documented behavior. But SVN isn't // happy about it. SVN will exit with code 1 and return the string below. if ($err != 0 && $stderr !== "svn: 'diff' returned 2\n") { throw new Exception( "svn diff returned unexpected error code: $err\n". "stdout: $stdout\n". "stderr: $stderr"); } if ($err == 0 && empty($stdout)) { // If there are no changes, 'diff' exits with no output, but that means // we can not distinguish between empty and unmodified files. Build a // synthetic "diff" without any changes in it. return $this->buildSyntheticUnchangedDiff($path); } return $stdout; } protected function buildSyntheticAdditionDiff($path, $source, $rev) { $type = $this->getSVNProperty($path, 'svn:mime-type'); if ($type == 'application/octet-stream') { return <<getPath($path))) { return null; } $data = Filesystem::readFile($this->getPath($path)); list($orig) = execx('svn cat %s@%s', $source, $rev); $src = new TempFile(); $dst = new TempFile(); Filesystem::writeFile($src, $orig); Filesystem::writeFile($dst, $data); list($err, $diff) = exec_manual( 'diff -L a/%s -L b/%s -U%d %s %s', str_replace($this->getSourceControlPath().'/', '', $source), $path, $this->getDiffLinesOfContext(), $src, $dst); if ($err == 1) { // 1 means there are differences. return <<buildSyntheticUnchangedDiff($path); } } protected function buildSyntheticUnchangedDiff($path) { $full_path = $this->getPath($path); if (is_dir($full_path)) { return null; } $data = Filesystem::readFile($full_path); $lines = explode("\n", $data); $len = count($lines); foreach ($lines as $key => $line) { $lines[$key] = ' '.$line; } $lines = implode("\n", $lines); return <<getPath(), $path); $stdout = trim($stdout); if (!strlen($stdout)) { // Empty file. return $blame; } foreach (explode("\n", $stdout) as $line) { $m = array(); if (!preg_match('/^\s*(\d+)\s+(\S+)/', $line, $m)) { throw new Exception("Bad blame? `{$line}'"); } $revision = $m[1]; $author = $m[2]; $blame[] = array($author, $revision); } return $blame; } - + public function getOriginalFileData($path) { // SVN issues warnings for nonexistent paths, directories, etc., but still // returns no error code. However, for new paths in the working copy it // fails. Assume that failure means the original file does not exist. list($err, $stdout) = exec_manual( '(cd %s && svn cat %s@)', $this->getPath(), $path); if ($err) { return null; } return $stdout; } - + public function getCurrentFileData($path) { $full_path = $this->getPath($path); if (Filesystem::pathExists($full_path)) { return Filesystem::readFile($full_path); } return null; } } diff --git a/src/unit/engine/phutil/PhutilUnitTestEngine.php b/src/unit/engine/phutil/PhutilUnitTestEngine.php index 8938e086..41c9ca8d 100644 --- a/src/unit/engine/phutil/PhutilUnitTestEngine.php +++ b/src/unit/engine/phutil/PhutilUnitTestEngine.php @@ -1,105 +1,112 @@ getPaths() as $path) { $library_root = phutil_get_library_root_for_path($path); if (!$library_root) { continue; } $library_name = phutil_get_library_name_for_root($library_root); $path = Filesystem::resolvePath($path); if ($path == $library_root) { continue; } if (!is_dir($path)) { $path = dirname($path); } $library_path = Filesystem::readablePath($path, $library_root); if (basename($library_path) == '__tests__') { // Okay, this is a __tests__ module. } else { - if (phutil_module_exists($library_name, $library_path.'/__tests__')) { + $exists = $bootloader->moduleExists( + $library_name, + $library_path.'/__tests__'); + if ($exists) { // This is a module which has a __tests__ module in it. $path .= '/__tests__'; } else { // Look for a parent named __tests__. $rpos = strrpos($library_path, '/__tests__'); if ($rpos === false) { // No tests to run since there is no child or parent module named // __tests__. continue; } // Select the parent named __tests__. $path = substr($path, 0, $rpos + strlen('/__tests__')); } } $module_name = Filesystem::readablePath($path, $library_root); $module_key = $library_name.':'.$module_name; $tests[$module_key] = array( 'library' => $library_name, 'root' => $library_root, 'module' => $module_name, ); } if (!$tests) { throw new ArcanistNoEffectException("No tests to run."); } $run_tests = array(); - $all_test_classes = phutil_find_class_descendants('ArcanistPhutilTestCase'); - $all_test_classes = array_fill_keys($all_test_classes, true); foreach ($tests as $test) { - $local_classes = phutil_find_classes_declared_in_module( - $test['library'], - $test['module']); - $local_classes = array_fill_keys($local_classes, true); - $run_tests += array_intersect($local_classes, $all_test_classes); + $symbols = id(new PhutilSymbolLoader()) + ->setType('class') + ->setLibrary($test['library']) + ->setModule($test['module']) + ->setAncestorClass('ArcanistPhutilTestCase') + ->selectAndLoadSymbols(); + foreach ($symbols as $symbol) { + $run_tests[$symbol['name']] = true; + } } $run_tests = array_keys($run_tests); if (!$run_tests) { throw new ArcanistNoEffectException( "No tests to run. You may need to rebuild the phutil library map."); } $results = array(); foreach ($run_tests as $test_class) { - phutil_autoload_class($test_class); + PhutilSymbolLoader::loadClass($test_class); $test_case = newv($test_class, array()); $results[] = $test_case->run(); } if ($results) { $results = call_user_func_array('array_merge', $results); } return $results; } } diff --git a/src/unit/engine/phutil/__init__.php b/src/unit/engine/phutil/__init__.php index 11320952..c7db7cc4 100644 --- a/src/unit/engine/phutil/__init__.php +++ b/src/unit/engine/phutil/__init__.php @@ -1,18 +1,18 @@ array( 'help' => "Show all lint warnings, not just those on changed lines." ), 'summary' => array( 'help' => "Show lint warnings in a more compact format." ), 'advice' => array( 'help' => "Show lint advice, not just warnings and errors." ), 'engine' => array( 'param' => 'classname', 'help' => "Override configured lint engine for this project." ), 'apply-patches' => array( 'help' => 'Apply patches suggested by lint to the working copy without '. 'prompting.', 'conflicts' => array( 'never-apply-patches' => true, ), ), 'never-apply-patches' => array( 'help' => 'Never apply patches suggested by lint.', 'conflicts' => array( 'apply-patches' => true, ), ), '*' => 'paths', ); } public function requiresWorkingCopy() { return true; } public function run() { $working_copy = $this->getWorkingCopy(); $engine = $this->getArgument('engine'); if (!$engine) { $engine = $working_copy->getConfig('lint_engine'); } $should_lint_all = $this->getArgument('lintall'); $repository_api = null; if (!$should_lint_all) { try { $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity( $working_copy); $this->setRepositoryAPI($repository_api); } catch (ArcanistUsageException $ex) { throw new ArcanistUsageException( $ex->getMessage()."\n\n". "Use '--lintall' to ignore working copy changes when running lint."); } if ($repository_api instanceof ArcanistSubversionAPI) { $paths = $repository_api->getWorkingCopyStatus(); $list = new FileList($this->getArgument('paths')); foreach ($paths as $path => $flags) { if (!$list->contains($path)) { unset($paths[$path]); } } } else { $this->parseGitRelativeCommit( $repository_api, $this->getArgument('paths')); $paths = $repository_api->getWorkingCopyStatus(); } foreach ($paths as $path => $flags) { if ($flags & ArcanistRepositoryAPI::FLAG_UNTRACKED) { unset($paths[$path]); } } $paths = array_keys($paths); } else { $paths = $this->getArgument('paths'); if (empty($paths)) { throw new ArcanistUsageException( "You must specify one or more files to lint when using '--lintall'."); } } if (!$engine) { throw new ArcanistNoEngineException( "No lint engine configured for this project. Edit .arcconfig to ". "specify a lint engine."); } - $ok = phutil_autoload_class($engine); - if (!$ok) { - throw new ArcanistUsageException( - "Configured lint engine '{$engine}' could not be loaded."); - } + PhutilSymbolLoader::loadClass($engine); $engine = newv($engine, array()); $engine->setWorkingCopy($working_copy); if ($this->getArgument('advice')) { $engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ADVICE); } else { $engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_WARNING); } $engine->setPaths($paths); if (!$should_lint_all) { foreach ($paths as $path) { $engine->setPathChangedLines( $path, $this->getChangedLines($path, 'new')); } } $results = $engine->run(); if ($this->getArgument('never-apply-patches')) { $apply_patches = false; } else { $apply_patches = true; } if ($this->getArgument('apply-patches')) { $prompt_patches = false; } else { $prompt_patches = true; } $wrote_to_disk = false; $renderer = new ArcanistLintRenderer(); if ($this->getArgument('summary')) { $renderer->setSummaryMode(true); } foreach ($results as $result) { if (!$result->getMessages()) { continue; } echo $renderer->renderLintResult($result); if ($apply_patches && $result->isPatchable()) { $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result); $old = $patcher->getUnmodifiedFileContent(); $new = $patcher->getModifiedFileContent(); if ($prompt_patches) { $old_file = $result->getFilePathOnDisk(); if (!Filesystem::pathExists($old_file)) { $old_file = '/dev/null'; } $new_file = new TempFile(); Filesystem::writeFile($new_file, $new); // TODO: Improve the behavior here, make it more like // difference_render(). passthru(csprintf("diff -u %s %s", $old_file, $new_file)); $prompt = phutil_console_format( "Apply this patch to __%s__?", $result->getPath()); if (!phutil_console_confirm($prompt, $default_no = false)) { continue; } } $patcher->writePatchToDisk(); $wrote_to_disk = true; } } if ($wrote_to_disk && ($repository_api instanceof ArcanistGitAPI)) { $amend = phutil_console_confirm("Amend HEAD with lint patches?"); if ($amend) { execx( '(cd %s; git commit -a --amend -C HEAD)', $repository_api->getPath()); } else { if ($this->getParentWorkflow()) { throw new ArcanistUsageException( "Sort out the lint changes that were applied to the working ". "copy and relint."); } } } $unresolved = array(); $result_code = self::RESULT_OKAY; foreach ($results as $result) { foreach ($result->getMessages() as $message) { if (!$message->isPatchApplied()) { if ($message->isError()) { $result_code = self::RESULT_ERRORS; break; } else if ($message->isWarning()) { if ($result_code != self::RESULT_ERRORS) { $result_code = self::RESULT_WARNINGS; } $unresolved[] = $message; } } } } $this->unresolvedMessages = $unresolved; if (!$this->getParentWorkflow()) { if ($result_code == self::RESULT_OKAY) { echo phutil_console_format( "** OKAY ** No lint warnings.\n"); } } return $result_code; } public function getUnresolvedMessages() { return $this->unresolvedMessages; } } diff --git a/src/workflow/lint/__init__.php b/src/workflow/lint/__init__.php index 9f6a024a..4560471f 100644 --- a/src/workflow/lint/__init__.php +++ b/src/workflow/lint/__init__.php @@ -1,27 +1,27 @@ 'svnargs', ); } public function run() { $svnargs = $this->getArgument('svnargs'); $repository = $svnargs[0]; $transaction = $svnargs[1]; list($commit_message) = execx( 'svnlook log --transaction %s %s', $transaction, $repository); // TODO: Do stuff with commit message. var_dump($commit_message); - + list($changed) = execx( 'svnlook changed --transaction %s %s', $transaction, $repository); - + $paths = array(); $changed = explode("\n", trim($changed)); foreach ($changed as $line) { $matches = null; preg_match('/^..\s*(.*)$/', $line, $matches); $paths[$matches[1]] = strlen($matches[1]); } - + $resolved = array(); $failed = array(); $missing = array(); $found = array(); asort($paths); - + foreach ($paths as $path => $length) { foreach ($resolved as $rpath => $root) { if (!strncmp($path, $rpath, strlen($rpath))) { $resolved[$path] = $root; continue 2; } } $config = $path; - + if (basename($config) == '.arcconfig') { $resolved[$config] = $config; continue; } - + $config = rtrim($config, '/'); $last_config = $config; do { if (!empty($missing[$config])) { break; } else if (!empty($found[$config])) { $resolved[$path] = $found[$config]; break; } list($err) = exec_manual( 'svnlook cat --transaction %s %s %s', $transaction, $repository, $config ? $config.'/.arcconfig' : '.arcconfig'); if ($err) { $missing[$path] = true; } else { $resolved[$path] = $config ? $config.'/.arcconfig' : '.arcconfig'; $found[$config] = $resolved[$path]; } $config = dirname($config); if ($config == '.') { $config = ''; } if ($config == $last_config) { break; } $last_config = $config; } while (true); - + if (empty($resolved[$path])) { $failed[] = $path; } } - + if ($failed && $resolved) { $failed_paths = ' '.implode("\n ", $failed); $resolved_paths = ' '.implode("\n ", array_keys($resolved)); throw new ArcanistUsageException( "This commit includes a mixture of files in Arcanist projects and ". "outside of Arcanist projects. A commit which affects an Arcanist ". "project must affect only that project.\n\n". "Files in projects:\n\n". $resolved_paths."\n\n". "Files not in projects:\n\n". $failed_paths); } - + if (!$resolved) { // None of the affected paths are beneath a .arcconfig file. return 3; } - + $groups = array(); foreach ($resolved as $path => $project) { $groups[$project][] = $path; } if (count($groups) > 1) { $message = array(); foreach ($groups as $config => $group) { $message[] = "Files underneath '{$config}':\n\n"; $message[] = " ".implode("\n ", $group)."\n\n"; } $message = implode('', $message); throw new ArcanistUsageException( "This commit includes a mixture of files from different Arcanist ". "projects. A commit which affects an Arcanist project must affect ". "only that project.\n\n". $message); } - + $project_root = key($groups); $paths = reset($groups); - + $data = array(); foreach ($paths as $path) { list($err, $filedata) = exec_manual( 'svnlook cat --transaction %s %s %s', $transaction, $repository, $path); $data[$path] = $err ? null : $filedata; } - + // TODO: Do stuff with data. var_dump($data); return 1; } } diff --git a/src/workflow/svn-hook-pre-commit/__init__.php b/src/workflow/svn-hook-pre-commit/__init__.php index 921b10ec..e3f69614 100644 --- a/src/workflow/svn-hook-pre-commit/__init__.php +++ b/src/workflow/svn-hook-pre-commit/__init__.php @@ -1,15 +1,16 @@ 'paths', ); } public function requiresWorkingCopy() { return true; } public function requiresRepositoryAPI() { return true; } 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."); } - $ok = phutil_autoload_class($engine_class); - if (!$ok) { - throw new ArcanistUsageException( - "Configured unit test engine '{$engine_class}' could not be loaded."); - } - $repository_api = $this->getRepositoryAPI(); if ($this->getArgument('paths')) { // TODO: deal with git stuff $paths = $this->getArgument('paths'); } else { $paths = $repository_api->getWorkingCopyStatus(); $paths = array_keys($paths); } + PhutilSymbolLoader::loadClass($engine_class); $engine = newv($engine_class, array()); $engine->setWorkingCopy($working_copy); $engine->setPaths($paths); $results = $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 **'), ); $unresolved = array(); foreach ($results as $result) { $result_code = $result->getResult(); echo $status_codes[$result_code].' '.$result->getName()."\n"; if ($result_code != ArcanistUnitTestResult::RESULT_PASS) { echo $result->getUserData()."\n"; $unresolved[] = $result; } } $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; } } return $overall_result; } public function getUnresolvedTests() { return $this->unresolvedTests; } } diff --git a/src/workflow/unit/__init__.php b/src/workflow/unit/__init__.php index 9ac1daea..b408e2ae 100644 --- a/src/workflow/unit/__init__.php +++ b/src/workflow/unit/__init__.php @@ -1,19 +1,18 @@