Changeset View
Changeset View
Standalone View
Standalone View
arcanist/unit/ABCUnitTestEngine.php
- This file was added.
<?php | |||||
/** | |||||
* Builds, runs, and interprets bitcoin unit test & coverage results. | |||||
*/ | |||||
final class ABCUnitTestEngine extends ArcanistUnitTestEngine { | |||||
private $unitTestDirectories = [ | |||||
'src/test', | |||||
'src/rpc/test', | |||||
'src/wallet/test', | |||||
]; | |||||
private $notATest = [ | |||||
'src/test/jsonutil.cpp', | |||||
'src/test/scriptflags.cpp', | |||||
'src/test/sigutil.cpp', | |||||
'src/test/test_bitcoin_fuzzt.cpp', | |||||
'src/test/test_bitcoin_main.cpp', | |||||
'src/test/test_bitcoin.cpp', | |||||
]; | |||||
private $projectRoot; | |||||
private $buildDir; | |||||
private $dependencies; | |||||
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; | |||||
} | |||||
private function isInTree($file) { | |||||
$path = Filesystem::resolvePath($file, $this->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['<Dependency n>'] = 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); | |||||
} | |||||
} |