diff --git a/.arcunit b/.arcunit new file mode 100644 --- /dev/null +++ b/.arcunit @@ -0,0 +1,11 @@ +{ + "engines": { + "bitcoin": { + "type": "bitcoin-test-engine", + "include": "(^src/.*\\.(h|cpp)$)", + "exclude": [ + "(^src/(secp256k1|univalue|leveldb)/)" + ] + } + } +} \ No newline at end of file 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,"21dded7bc13561b46b1b39ecbfaa6a3f":{"have":{"class":{"ClangFormatLinter":79}},"need":{"function":{"pht":302,"id":1342},"class":{"ArcanistExternalLinter":105,"ArcanistLintMessage":1349,"Filesystem":1193,"ArcanistLinter":1456,"ArcanistLintSeverity":1534}},"xmap":{"ClangFormatLinter":["ArcanistExternalLinter"]}},"d60c8224f471e0ecddc2a6f3c6839cd1":{"have":{"class":{"AutoPEP8FormatLinter":75}},"need":{"function":{"pht":297,"id":1317},"class":{"ArcanistExternalLinter":104,"ArcanistLintMessage":1324,"Filesystem":1168,"ArcanistLinter":1431,"ArcanistLintSeverity":1509}},"xmap":{"AutoPEP8FormatLinter":["ArcanistExternalLinter"]}}} \ No newline at end of file +{"__symbol_cache_version__":11,"d60c8224f471e0ecddc2a6f3c6839cd1":{"have":{"class":{"AutoPEP8FormatLinter":75}},"need":{"function":{"pht":297,"id":1317},"class":{"ArcanistExternalLinter":104,"ArcanistLintMessage":1324,"Filesystem":1168,"ArcanistLinter":1431,"ArcanistLintSeverity":1509}},"xmap":{"AutoPEP8FormatLinter":["ArcanistExternalLinter"]}},"213c3145da34ed6dfc0d70d628a2a086":{"have":{"class":{"CheckDocLinter":106}},"need":{"function":{"pht":323,"id":1868},"class":{"ArcanistExternalLinter":129,"ArcanistLintMessage":1875,"Filesystem":737,"ArcanistLinter":1923,"ArcanistLintSeverity":2009}},"xmap":{"CheckDocLinter":["ArcanistExternalLinter"]}},"7bab1f879b8a86dd9977b8c0d075935f":{"have":{"class":{"ClangFormatLinter":79}},"need":{"function":{"pht":302,"execx":787,"id":1664},"class":{"ArcanistExternalLinter":105,"ArcanistLintMessage":1671,"Filesystem":1515,"ArcanistLinter":1778,"ArcanistLintSeverity":1856}},"xmap":{"ClangFormatLinter":["ArcanistExternalLinter"]}},"eb7e74d16adb568d20df19ef87ef4ff7":{"have":{"class":{"ABCUnitTestEngine":96}},"need":{"function":{"pht":2800,"id":3034,"array_mergev":3905},"class":{"ArcanistUnitTestEngine":122,"ExecFuture":2108,"ArcanistUsageException":2768,"ABCTestResultParser":3041}},"xmap":{"ABCUnitTestEngine":["ArcanistUnitTestEngine"]}},"15794ecbaa1375dfc4e61065c469a370":{"have":{"class":{"ABCTestResultParser":19}},"need":{"function":{"id":690},"class":{"ArcanistTestResultParser":47,"ArcanistUnitTestResult":697}},"xmap":{"ABCTestResultParser":["ArcanistTestResultParser"]}}} \ 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,12 +9,16 @@ phutil_register_library_map(array( '__library_version__' => 2, 'class' => array( + 'ABCTestResultParser' => 'unit/ABCTestResultParser.php', + 'ABCUnitTestEngine' => 'unit/ABCUnitTestEngine.php', 'AutoPEP8FormatLinter' => 'linter/AutoPEP8Linter.php', 'CheckDocLinter' => 'linter/CheckDocLinter.php', 'ClangFormatLinter' => 'linter/ClangFormatLinter.php', ), 'function' => array(), 'xmap' => array( + 'ABCTestResultParser' => 'ArcanistTestResultParser', + 'ABCUnitTestEngine' => 'ArcanistUnitTestEngine', 'AutoPEP8FormatLinter' => 'ArcanistExternalLinter', 'CheckDocLinter' => 'ArcanistExternalLinter', 'ClangFormatLinter' => 'ArcanistExternalLinter', 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,49 @@ +setName($test) + ->setDuration($output['duration']) + ->setResult(ArcanistUnitTestResult::RESULT_PASS)); + } + + $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, "ABCTestResultParser::reduceErrors", array()); + + $results = []; + foreach ($reducedErrors as $testName => $userData) { + $results[] = id(new ArcanistUnitTestResult()) + ->setName($testName) + ->setUserData($userData) + ->setResult(ArcanistUnitTestResult::RESULT_BROKEN); + } + + return $results; + } +} + +?> \ No newline at end of file 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,153 @@ +buildDirectory = getcwd(); + $this->testBinary = $this->buildDirectory.'/src/test/test_bitcoin'; + if (is_executable($this->testBinary)) { + return true; + } + + $this->buildDirectory = null; + $this->testBinary = null; + $recentTime = 0; + $dirs = glob($this->projectRoot.'/*', GLOB_ONLYDIR); + foreach($dirs as $dir) { + $testPath = $dir.'/src/test/test_bitcoin'; + if (is_executable($testPath)) { + $lastModificationTime = filemtime($testPath); + if ($lastModificationTime > $recentTime) { + $this->buildDirectory = $dir; + $this->testBinary = $testPath; + $recentTime = $lastModificationTime; + } + } + } + + if (!is_null($this->testBinary)) { + return true; + } + + return false; + } + + private function needsBuild($file) { + $filePath = FileSystem::resolvePath($file, $this->projectRoot); + return filemtime($filePath) > filemtime($this->testBinary); + } + + private function findTest($file) { + $pathinfo = pathinfo($file); + // If the modifications is on a test file, return the test directly + if (substr($pathinfo['basename'], -10) == '_tests.cpp' && + (($pathinfo['dirname'] === 'src/test') || + ($pathinfo['dirname'] === 'src/rpc/test') || + ($pathinfo['dirname'] === 'src/wallet/test'))) { + return $pathinfo['filename']; + } + + // Otherwise search if a corresponding test exists + $testName = $pathinfo['filename'].'_tests'; + $testFile = $testName.'.cpp'; + $tests = scandir($this->getWorkingCopy()->getProjectPath('/src/test')); + if (in_array($testFile, $tests)) { + return $testName; + } + return NULL; + } + + private function makeTestCommand($test) { + return $this->testBinary.' --run_test='.$test; + } + + private function executeCommand($cmd) { + $timeStart = microtime(true); + $future = new ExecFuture($cmd); + list($ret, $stdout, $stderr) = $future->resolve(); + $timeEnd = microtime(true); + + return ['return' => $ret, 'stdout' => $stdout, 'stderr' => $stderr, + 'duration' => $timeEnd - $timeStart]; + } + + public function getEngineConfigurationName() { + return 'bitcoin-test-engine'; + } + + protected function supportsRunAllTests() { + return true; + } + + public function shouldEchoTestResults() { + // i.e. this engine does not output its own results. + return false; + } + + public function run() { + $this->projectRoot = $this->getWorkingCopy()->getProjectRoot(); + + if (!$this->findBuildDirectoryAndBinary()) { + throw new ArcanistUsageException( + pht("Unable to find test_bitcoin binary.\n". + "Make sure it has been compiled and cd to your build folder")); + } + + if ($this->getRunAllTests()) { + $output = $this->executeCommand($this->testBinary); + return id(new ABCTestResultParser()) + ->setEnableCoverage(false) + ->setProjectRoot($this->projectRoot) + ->setStderr($output['stderr']) + ->parseTestResults('All unit tests', $output); + } + + $paths = $this->getPaths(); + if (empty($paths)) { + return array(); + } + + $tests = []; + foreach ($paths as $path) { + /* Check that the file has been built before searching for a test */ + if ($this->needsBuild($path)) { + throw new ArcanistUsageException( + pht("The file %s has been modified since last build.\n". + "Please rebuild before running the unit tests", $path)); + } + $test = $this->findTest($path); + if (!is_null($test)) { + $tests[] = $test; + } + } + $tests = array_unique($tests); + + $results = []; + foreach ($tests as $test) { + $cmd = $this->makeTestCommand($test); + $output = $this->executeCommand($cmd); + $results[] = id(new ABCTestResultParser()) + ->setEnableCoverage(false) + ->setProjectRoot($this->projectRoot) + ->setStderr($output['stderr']) + ->parseTestResults($test, $output); + } + + return array_mergev($results); + } +} \ No newline at end of file