diff --git a/.arcconfig b/.arcconfig --- a/.arcconfig +++ b/.arcconfig @@ -7,5 +7,6 @@ "arc.feature.start.default" : "master", "history.immutable" : false, "load" : ["arcanist"], - "build_directory": "build-cmake" + "build_directory": "build-cmake", + "arcanist_configuration" : "ArcanistBitcoinABCConfiguration" } diff --git a/.arcunit b/.arcunit new file mode 100644 --- /dev/null +++ b/.arcunit @@ -0,0 +1,8 @@ +{ + "engines": { + "bitcoin": { + "type": "bitcoin-test-engine", + "include": "(^src/.*\\.(h|c|cpp)$)" + } + } +} diff --git a/arcanist/.phutil_module_cache b/arcanist/.phutil_module_cache --- a/arcanist/.phutil_module_cache +++ b/arcanist/.phutil_module_cache @@ -1 +1 @@ -{"__symbol_cache_version__":11,"90a8b110dc475955f15bb81d37268cb5":{"have":{"class":{"AutoPEP8FormatLinter":75}},"need":{"function":{"pht":297,"execx":769,"id":1903},"class":{"ArcanistExternalLinter":104,"ArcanistLintMessage":1910,"Filesystem":1754,"ArcanistLinter":2017,"ArcanistLintSeverity":2095}},"xmap":{"AutoPEP8FormatLinter":["ArcanistExternalLinter"]}},"bf0805c02029a7226e8c0d7dee039b3c":{"have":{"class":{"CheckDocLinter":106}},"need":{"function":{"pht":323,"id":1847},"class":{"ArcanistExternalLinter":129,"ArcanistLintMessage":1854,"Filesystem":731,"ArcanistLinter":1902,"ArcanistLintSeverity":1988}},"xmap":{"CheckDocLinter":["ArcanistExternalLinter"]}},"6af7410cfea496ff1d4dcc2624b6b8ea":{"have":{"class":{"ClangFormatLinter":79}},"need":{"function":{"pht":302,"execx":781,"id":1653},"class":{"ArcanistExternalLinter":105,"ArcanistLintMessage":1660,"Filesystem":1504,"ArcanistLinter":1767,"ArcanistLintSeverity":1845}},"xmap":{"ClangFormatLinter":["ArcanistExternalLinter"]}},"c8d28781ae8aa129cc38d505095f7f45":{"have":{"class":{"LocaleDependenceLinter":155}},"need":{"function":{"pht":579,"id":3348},"class":{"ArcanistExternalLinter":186,"ArcanistLintMessage":3355,"Filesystem":1035,"ArcanistLinter":3405,"ArcanistLintSeverity":3564}},"xmap":{"LocaleDependenceLinter":["ArcanistExternalLinter"]}},"6f2f22dd0f259fb2eaa284b4fab3bc29":{"have":{"class":{"PythonFormatLinter":123}},"need":{"function":{"pht":353,"id":1838},"class":{"ArcanistExternalLinter":150,"ArcanistLintMessage":1845,"Filesystem":776,"ArcanistLinter":1970,"ArcanistLintSeverity":2053}},"xmap":{"PythonFormatLinter":["ArcanistExternalLinter"]}},"25781df78f6eebfb223296b8265e9d19":{"have":{"class":{"TestsLinter":103}},"need":{"function":{"pht":318,"id":2629},"class":{"ArcanistExternalLinter":123,"ArcanistLintMessage":2636,"Filesystem":776,"ArcanistLinter":2684,"ArcanistLintSeverity":2792}},"xmap":{"TestsLinter":["ArcanistExternalLinter"]}},"74ec116ee9ddb8d1b9e1ac3568d523ab":{"have":{"class":{"ArcanistBuildWorkflow":48}},"need":{"function":{"phutil_console_format":320,"pht":775},"class":{"ArcanistWorkflow":78,"ExecFuture":3740,"PhutilConsole":1585,"Filesystem":2718},"class\/interface":{"FilesystemException":2876}},"xmap":{"ArcanistBuildWorkflow":["ArcanistWorkflow"]}}} \ No newline at end of file +{"__symbol_cache_version__":11,"90a8b110dc475955f15bb81d37268cb5":{"have":{"class":{"AutoPEP8FormatLinter":75}},"need":{"function":{"pht":297,"execx":769,"id":1903},"class":{"ArcanistExternalLinter":104,"ArcanistLintMessage":1910,"Filesystem":1754,"ArcanistLinter":2017,"ArcanistLintSeverity":2095}},"xmap":{"AutoPEP8FormatLinter":["ArcanistExternalLinter"]}},"bf0805c02029a7226e8c0d7dee039b3c":{"have":{"class":{"CheckDocLinter":106}},"need":{"function":{"pht":323,"id":1847},"class":{"ArcanistExternalLinter":129,"ArcanistLintMessage":1854,"Filesystem":731,"ArcanistLinter":1902,"ArcanistLintSeverity":1988}},"xmap":{"CheckDocLinter":["ArcanistExternalLinter"]}},"6af7410cfea496ff1d4dcc2624b6b8ea":{"have":{"class":{"ClangFormatLinter":79}},"need":{"function":{"pht":302,"execx":781,"id":1653},"class":{"ArcanistExternalLinter":105,"ArcanistLintMessage":1660,"Filesystem":1504,"ArcanistLinter":1767,"ArcanistLintSeverity":1845}},"xmap":{"ClangFormatLinter":["ArcanistExternalLinter"]}},"c8d28781ae8aa129cc38d505095f7f45":{"have":{"class":{"LocaleDependenceLinter":155}},"need":{"function":{"pht":579,"id":3348},"class":{"ArcanistExternalLinter":186,"ArcanistLintMessage":3355,"Filesystem":1035,"ArcanistLinter":3405,"ArcanistLintSeverity":3564}},"xmap":{"LocaleDependenceLinter":["ArcanistExternalLinter"]}},"6f2f22dd0f259fb2eaa284b4fab3bc29":{"have":{"class":{"PythonFormatLinter":123}},"need":{"function":{"pht":353,"id":1838},"class":{"ArcanistExternalLinter":150,"ArcanistLintMessage":1845,"Filesystem":776,"ArcanistLinter":1970,"ArcanistLintSeverity":2053}},"xmap":{"PythonFormatLinter":["ArcanistExternalLinter"]}},"25781df78f6eebfb223296b8265e9d19":{"have":{"class":{"TestsLinter":103}},"need":{"function":{"pht":318,"id":2629},"class":{"ArcanistExternalLinter":123,"ArcanistLintMessage":2636,"Filesystem":776,"ArcanistLinter":2684,"ArcanistLintSeverity":2792}},"xmap":{"TestsLinter":["ArcanistExternalLinter"]}},"6a61ff7933e1ebd9bfab45c3dda94146":{"have":{"class":{"ArcanistBitcoinABCConfiguration":13}},"need":{"class":{"ArcanistConfiguration":53},"class\/interface":{"ArcanistWorkflow":122}},"xmap":{"ArcanistBitcoinABCConfiguration":["ArcanistConfiguration"]}},"85b955b510d6eab45d75f7bdce83d8fd":{"have":{"class":{"ABCTestResultParser":19}},"need":{"function":{"id":624},"class":{"ArcanistTestResultParser":47,"ArcanistUnitTestResult":631}},"xmap":{"ABCTestResultParser":["ArcanistTestResultParser"]}},"1e68ab0e616dd47a08df23a4b5565149":{"have":{"class":{"ABCUnitTestEngine":96}},"need":{"function":{"id":6518,"exec_manual":6817},"class":{"ArcanistUnitTestEngine":122,"ExecFuture":2014,"ABCTestResultParser":6525,"Filesystem":876}},"xmap":{"ABCUnitTestEngine":["ArcanistUnitTestEngine"]}},"74ec116ee9ddb8d1b9e1ac3568d523ab":{"have":{"class":{"ArcanistBuildWorkflow":48}},"need":{"function":{"phutil_console_format":320,"pht":775},"class":{"ArcanistWorkflow":78,"ExecFuture":3740,"PhutilConsole":1585,"Filesystem":2718},"class\/interface":{"FilesystemException":2876}},"xmap":{"ArcanistBuildWorkflow":["ArcanistWorkflow"]}}} \ No newline at end of file diff --git a/arcanist/__phutil_library_map__.php b/arcanist/__phutil_library_map__.php --- a/arcanist/__phutil_library_map__.php +++ b/arcanist/__phutil_library_map__.php @@ -9,6 +9,9 @@ phutil_register_library_map(array( '__library_version__' => 2, 'class' => array( + 'ABCTestResultParser' => 'unit/ABCTestResultParser.php', + 'ABCUnitTestEngine' => 'unit/ABCUnitTestEngine.php', + 'ArcanistBitcoinABCConfiguration' => 'configuration/ArcanistBitcoinABCConfiguration.php', 'ArcanistBuildWorkflow' => 'workflow/ArcanistBuildWorkflow.php', 'AutoPEP8FormatLinter' => 'linter/AutoPEP8Linter.php', 'CheckDocLinter' => 'linter/CheckDocLinter.php', @@ -19,6 +22,9 @@ ), 'function' => array(), 'xmap' => array( + 'ABCTestResultParser' => 'ArcanistTestResultParser', + 'ABCUnitTestEngine' => 'ArcanistUnitTestEngine', + 'ArcanistBitcoinABCConfiguration' => 'ArcanistConfiguration', 'ArcanistBuildWorkflow' => 'ArcanistWorkflow', 'AutoPEP8FormatLinter' => 'ArcanistExternalLinter', 'CheckDocLinter' => 'ArcanistExternalLinter', diff --git a/arcanist/configuration/ArcanistBitcoinABCConfiguration.php b/arcanist/configuration/ArcanistBitcoinABCConfiguration.php new file mode 100644 --- /dev/null +++ b/arcanist/configuration/ArcanistBitcoinABCConfiguration.php @@ -0,0 +1,13 @@ +buildChildWorkflow('build', + array('test_bitcoin')); + return $buildWorkflow->run(); + } + return 0; + } +} diff --git a/arcanist/unit/ABCTestResultParser.php b/arcanist/unit/ABCTestResultParser.php new file mode 100644 --- /dev/null +++ b/arcanist/unit/ABCTestResultParser.php @@ -0,0 +1,61 @@ +setName('No unit test to run') + ->setResult(ArcanistUnitTestResult::RESULT_SKIP)); + } + + /* + * Display duration on success. + * This is useless to list all the passed tests in this case. + */ + if ($output['return'] == 0) { + return array(id(new ArcanistUnitTestResult()) + ->setName('Bitcoin unit tests') + ->setDuration($output['duration']) + ->setResult(ArcanistUnitTestResult::RESULT_PASS)); + } + + /* + * Find the errors in the output. + * The test name is extracted from the output; the error messages are + * grouped by file. + */ + $pattern = + '/.+\/test\/(\w+.cpp)\((\d+)\): error: in "(.+)": check (.+) has failed/'; + preg_match_all($pattern, $output['stdout'], $errors, + $flags = PREG_SET_ORDER); + + $reducedErrors = array_reduce($errors, [__CLASS__, 'reduceErrors'], + array()); + + $results = []; + foreach ($reducedErrors as $testName => $userData) { + $results[] = id(new ArcanistUnitTestResult()) + ->setName($testName) + ->setUserData($userData) + ->setResult(ArcanistUnitTestResult::RESULT_FAIL); + } + + return $results; + } +} diff --git a/arcanist/unit/ABCUnitTestEngine.php b/arcanist/unit/ABCUnitTestEngine.php new file mode 100644 --- /dev/null +++ b/arcanist/unit/ABCUnitTestEngine.php @@ -0,0 +1,228 @@ +buildDir); + return (file_exists($path) && + (substr($path, 0, strlen($this->projectRoot)) === $this->projectRoot)); + } + + private function isTest($file) { + /* + * Special cases: + * these files are located in a test folder but are not tests. Skip them. + */ + foreach ($this->notATest as $relativeTestFile) { + $absoluteTestFile = Filesystem::resolvePath($relativeTestFile, + $this->projectRoot); + if (Filesystem::pathsAreEquivalent($file, $absoluteTestFile)) { + return false; + } + } + + /* Search if the file is located under any of the test repositories */ + foreach($this->unitTestDirectories as $relativeTestDir) { + $absoluteTestDir = Filesystem::resolvePath($relativeTestDir, + $this->projectRoot); + + $test = Filesystem::resolvePath($file, $absoluteTestDir); + if ((file_exists($test)) && + (substr($test, 0, strlen($absoluteTestDir)) === $absoluteTestDir)) { + return true; + } + } + + return false; + } + + private function BuildDependenciesLookup() { + $this->dependencies = []; + + $future = new ExecFuture('ninja -t deps'); + $future->setCWD($this->buildDir); + list($ret, $stdout) = $future->resolve(); + + if ($ret != 0) { + return; + } + + /* + * Parse the stdout to build a dependency array. + * The output format is: + * Target file 1 in the build directory + * Dependency 1 for the target 1 + * Dependency 2 for the target 1 + * ... + * Target file 2 in the build directory + * Dependency 1 for the target 2 + * ... + * + * This output is parsed in order to get an associative array which tracks + * the dependencies in the reversed order, from a dependency to its targets: + * dependencies[''] = array('target 1', 'target 2', ...) + * + * The target path is translated from a build directory object path to + * working tree source file. + * + * All the paths (dependencies and targets) are converted to absolute paths. + */ + foreach (preg_split("/((\r?\n)|(\r\n?))/", $stdout) as $line) { + /* + * The is the beginning of the dependency section for this target. + * The next lines contains the dependencies to build this target. + */ + if (preg_match('/(.+)\/CMakeFiles\/.+\.dir\/(.+)\.o: #deps (\d+),/', + $line, $matches)) { + list(, $prefix, $target) = $matches; + $target = preg_replace('/__\//', '../', $target); + $target = Filesystem::resolvePath($prefix.'/'.$target, + $this->projectRoot); + } + + /* The is one of the dependencies for the current target. */ + if (preg_match('/\s+(.+(\.h))$/', $line, $matches)) { + list(, $dependency) = $matches; + $dependency = Filesystem::resolvePath($dependency, $this->buildDir); + if ($this->isInTree($dependency)) { + $this->dependencies[$dependency][] = $target; + } + } + } + } + + public function run() { + $this->projectRoot = $this->getWorkingCopy()->getProjectRoot(); + + /* + * Workaround for a bug: the unit test engine configuration manager will not + * be set if ArcanistConfigurationDrivenUnitTestEngine is used. + * https://secure.phabricator.com/D19465 + */ + $buildDir = $this->getWorkingCopy()->getProjectConfig( + 'build_directory'); + + /* + * Get the build directory from the configuration. + * + * Uncomment this line when the above mentionned bug is fixed: + * $buildDir = $this->getConfigurationManager()->getConfigFromAnySource( + * 'build_directory'); + */ + + if (!$buildDir) { + return $this->setFailure( + 'No build directory is configured. Set the `build_directory` option '. + 'in you configuration file and run `arc unit` again.'); + } + $this->buildDir = Filesystem::resolvePath($buildDir, $this->projectRoot); + + /* Build the dependency graph using Ninja built-in tools */ + $this->BuildDependenciesLookup(); + + $impactedFiles = []; + + if ($this->getRunAllTests()) { + /* + * If `arc unit` is run with the `--everything` argument, consider all + * the files as impacted. + */ + $impactedFiles = array_merge(...array_values($this->dependencies)); + } else { + /* + * List impacted files from the modified files. + * 1/ Add the modified file itself to the impacted files + * 2/ Build the header file path from the modified file (if this is + * already a header, it is unchanged). + * 3/ If the header path exist, get the list of dependent files from the + * dependency graph. + * 4/ Add all these dependent files to the list of impacted files. + */ + $paths = $this->getPaths(); + + foreach ($paths as $path) { + $absolutePath = Filesystem::resolvePath($path, $this->projectRoot); + $impactedFiles[] = $absolutePath; + + $headerPath = preg_replace('/(.+).cpp$/', '$1.h', $absolutePath); + if (is_file($headerPath)) { + $impactedFiles = array_merge($impactedFiles, + $this->dependencies[$headerPath]); + } + } + } + + $impactedFiles = array_unique($impactedFiles); + + /* + * Filter the impacted files to determine which one is actually a test. + * This is done by checking if the file is located in one of the test + * directories. + */ + $tests = []; + foreach ($impactedFiles as $file) { + if ($this->isTest($file)) { + $tests[] = basename($file, '.cpp'); + } + } + + /* If there is no test to run, return */ + if (empty($tests)) { + return id(new ABCTestResultParser()) + ->parseTestResults($tests, null); + } + + /* Run test_bitcoin */ + $testBinary = Filesystem::resolvePath('src/test/test_bitcoin', + $this->buildDir); + $cmd = $testBinary.' -t %s'; + $timeStart = microtime(true); + list($ret, $stdout, $stderr) = exec_manual($cmd, implode(',', $tests)); + $timeEnd = microtime(true); + $output = [ + 'return' => $ret, + 'stdout' => $stdout, + 'duration' => $timeEnd - $timeStart + ]; + + /* Parse the result to generate the output messages */ + return id(new ABCTestResultParser()) + ->setEnableCoverage(false) + ->setProjectRoot($this->projectRoot) + ->setStderr($stderr) + ->parseTestResults($tests, $output); + } +}