mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-26 00:32:41 +01:00
[Wilds] Make more test cases (mostly related to the phutil -> arcanist move) pass
Summary: Ref T13098. Makes some tests pass by updating `'phutil'` to `'arcanist'`. Skips some tests which won't pass for a while. Also removes external test engines for now since they aren't realistically going to run for a while and they significantly complicate bootstrapping a set of passing tests out of `arc unit`. Test Plan: Ran `arc unit`, saw fewer failures. Reviewers: amckinley Reviewed By: amckinley Subscribers: aurelijus Maniphest Tasks: T13098 Differential Revision: https://secure.phabricator.com/D19714
This commit is contained in:
parent
fe8f0aea9c
commit
a3e29773df
20 changed files with 23 additions and 1429 deletions
|
@ -293,7 +293,6 @@ phutil_register_library_map(array(
|
|||
'ArcanistLesscLinter' => 'lint/linter/ArcanistLesscLinter.php',
|
||||
'ArcanistLesscLinterTestCase' => 'lint/linter/__tests__/ArcanistLesscLinterTestCase.php',
|
||||
'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php',
|
||||
'ArcanistLibraryTestCase' => '__tests__/ArcanistLibraryTestCase.php',
|
||||
'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php',
|
||||
'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php',
|
||||
'ArcanistLintMessageTestCase' => 'lint/__tests__/ArcanistLintMessageTestCase.php',
|
||||
|
@ -521,7 +520,6 @@ phutil_register_library_map(array(
|
|||
'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php',
|
||||
'ArcanistXUnitTestResultParser' => 'unit/parser/ArcanistXUnitTestResultParser.php',
|
||||
'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
|
||||
'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php',
|
||||
'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
|
||||
'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
|
||||
'CommandException' => 'future/exec/CommandException.php',
|
||||
|
@ -558,7 +556,6 @@ phutil_register_library_map(array(
|
|||
'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
|
||||
'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
|
||||
'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
|
||||
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
|
||||
'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php',
|
||||
'PhageAction' => 'phage/action/PhageAction.php',
|
||||
'PhageAgentAction' => 'phage/action/PhageAgentAction.php',
|
||||
|
@ -572,8 +569,6 @@ phutil_register_library_map(array(
|
|||
'PhageWorkflow' => 'phage/workflow/PhageWorkflow.php',
|
||||
'Phobject' => 'object/Phobject.php',
|
||||
'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php',
|
||||
'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php',
|
||||
'PhpunitTestEngineTestCase' => 'unit/engine/__tests__/PhpunitTestEngineTestCase.php',
|
||||
'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php',
|
||||
'PhutilAWSCloudFormationFuture' => 'future/aws/PhutilAWSCloudFormationFuture.php',
|
||||
'PhutilAWSCloudWatchFuture' => 'future/aws/PhutilAWSCloudWatchFuture.php',
|
||||
|
@ -964,7 +959,6 @@ phutil_register_library_map(array(
|
|||
'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
|
||||
'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
|
||||
'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
|
||||
'PytestTestEngine' => 'unit/engine/PytestTestEngine.php',
|
||||
'QueryFuture' => 'future/query/QueryFuture.php',
|
||||
'TempFile' => 'filesystem/TempFile.php',
|
||||
'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
|
||||
|
@ -974,7 +968,6 @@ phutil_register_library_map(array(
|
|||
'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
|
||||
'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
|
||||
'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
|
||||
'XUnitTestEngine' => 'unit/engine/XUnitTestEngine.php',
|
||||
'XUnitTestResultParserTestCase' => 'unit/parser/__tests__/XUnitTestResultParserTestCase.php',
|
||||
'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php',
|
||||
),
|
||||
|
@ -1404,7 +1397,6 @@ phutil_register_library_map(array(
|
|||
'ArcanistLesscLinter' => 'ArcanistExternalLinter',
|
||||
'ArcanistLesscLinterTestCase' => 'ArcanistExternalLinterTestCase',
|
||||
'ArcanistLiberateWorkflow' => 'ArcanistWorkflow',
|
||||
'ArcanistLibraryTestCase' => 'PhutilLibraryTestCase',
|
||||
'ArcanistLintEngine' => 'Phobject',
|
||||
'ArcanistLintMessage' => 'Phobject',
|
||||
'ArcanistLintMessageTestCase' => 'PhutilTestCase',
|
||||
|
@ -1632,7 +1624,6 @@ phutil_register_library_map(array(
|
|||
'ArcanistXMLLinterTestCase' => 'ArcanistLinterTestCase',
|
||||
'ArcanistXUnitTestResultParser' => 'Phobject',
|
||||
'BaseHTTPFuture' => 'Future',
|
||||
'CSharpToolsTestEngine' => 'XUnitTestEngine',
|
||||
'CaseInsensitiveArray' => 'PhutilArray',
|
||||
'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
|
||||
'CommandException' => 'Exception',
|
||||
|
@ -1675,7 +1666,6 @@ phutil_register_library_map(array(
|
|||
'LinesOfALargeFile' => 'LinesOfALarge',
|
||||
'LinesOfALargeFileTestCase' => 'PhutilTestCase',
|
||||
'MFilterTestHelper' => 'Phobject',
|
||||
'NoseTestEngine' => 'ArcanistUnitTestEngine',
|
||||
'PHPASTParserTestCase' => 'PhutilTestCase',
|
||||
'PhageAction' => 'Phobject',
|
||||
'PhageAgentAction' => 'PhageAction',
|
||||
|
@ -1689,8 +1679,6 @@ phutil_register_library_map(array(
|
|||
'PhageWorkflow' => 'PhutilArgumentWorkflow',
|
||||
'Phobject' => 'Iterator',
|
||||
'PhobjectTestCase' => 'PhutilTestCase',
|
||||
'PhpunitTestEngine' => 'ArcanistUnitTestEngine',
|
||||
'PhpunitTestEngineTestCase' => 'PhutilTestCase',
|
||||
'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache',
|
||||
'PhutilAWSCloudFormationFuture' => 'PhutilAWSFuture',
|
||||
'PhutilAWSCloudWatchFuture' => 'PhutilAWSFuture',
|
||||
|
@ -2099,7 +2087,6 @@ phutil_register_library_map(array(
|
|||
'PhutilXHPASTSyntaxHighlighter' => 'Phobject',
|
||||
'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
|
||||
'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
|
||||
'PytestTestEngine' => 'ArcanistUnitTestEngine',
|
||||
'QueryFuture' => 'Future',
|
||||
'TempFile' => 'Phobject',
|
||||
'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
|
||||
|
@ -2109,7 +2096,6 @@ phutil_register_library_map(array(
|
|||
'XHPASTToken' => 'AASTToken',
|
||||
'XHPASTTree' => 'AASTTree',
|
||||
'XHPASTTreeTestCase' => 'PhutilTestCase',
|
||||
'XUnitTestEngine' => 'ArcanistUnitTestEngine',
|
||||
'XUnitTestResultParserTestCase' => 'PhutilTestCase',
|
||||
'XsprintfUnknownConversionException' => 'InvalidArgumentException',
|
||||
),
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistLibraryTestCase extends PhutilLibraryTestCase {}
|
|
@ -11,6 +11,8 @@ class PhutilLibraryTestCase extends PhutilTestCase {
|
|||
* missing methods in descendants of abstract base classes.
|
||||
*/
|
||||
public function testEverythingImplemented() {
|
||||
$this->assertSkipped('TOOLSETS: Many workflows are missing methods.');
|
||||
|
||||
id(new PhutilSymbolLoader())
|
||||
->setLibrary($this->getLibraryName())
|
||||
->selectAndLoadSymbols();
|
||||
|
@ -92,6 +94,8 @@ class PhutilLibraryTestCase extends PhutilTestCase {
|
|||
* parent class.
|
||||
*/
|
||||
public function testMethodVisibility() {
|
||||
$this->assertSkipped('TOOLSETS: Many workflows currently have failures.');
|
||||
|
||||
$symbols = id(new PhutilSymbolLoader())
|
||||
->setLibrary($this->getLibraryName())
|
||||
->selectSymbolsWithoutLoading();
|
||||
|
|
|
@ -95,8 +95,8 @@ final class PhutilDeferredLogTestCase extends PhutilTestCase {
|
|||
}
|
||||
|
||||
public function testManyWriters() {
|
||||
$root = phutil_get_library_root('phutil').'/../';
|
||||
$bin = $root.'scripts/test/deferred_log.php';
|
||||
$root = phutil_get_library_root('arcanist').'/../';
|
||||
$bin = $root.'support/unit/deferred_log.php';
|
||||
|
||||
$n_writers = 3;
|
||||
$n_lines = 8;
|
||||
|
|
|
@ -171,8 +171,8 @@ final class PhutilFileLockTestCase extends PhutilTestCase {
|
|||
}
|
||||
|
||||
private function buildLockFuture($flags, $file) {
|
||||
$root = dirname(phutil_get_library_root('phutil'));
|
||||
$bin = $root.'/scripts/utils/lock.php';
|
||||
$root = dirname(phutil_get_library_root('arcanist'));
|
||||
$bin = $root.'/support/unit/lock.php';
|
||||
|
||||
// NOTE: Use `exec` so this passes on Ubuntu, where the default `dash` shell
|
||||
// will eat any kills we send during the tests.
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
final class PhutilModuleUtilsTestCase extends PhutilTestCase {
|
||||
|
||||
public function testGetCurrentLibraryName() {
|
||||
$this->assertEqual('phutil', phutil_get_current_library_name());
|
||||
$this->assertEqual('arcanist', phutil_get_current_library_name());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@ final class PhutilJSONParser extends Phobject {
|
|||
}
|
||||
|
||||
public function parse($json) {
|
||||
$jsonlint_root = phutil_get_library_root('phutil').'/../externals/jsonlint';
|
||||
$arcanist_root = phutil_get_library_root('arcanist');
|
||||
|
||||
$jsonlint_root = $arcanist_root.'/../externals/jsonlint';
|
||||
require_once $jsonlint_root.'/src/Seld/JsonLint/JsonParser.php';
|
||||
require_once $jsonlint_root.'/src/Seld/JsonLint/Lexer.php';
|
||||
require_once $jsonlint_root.'/src/Seld/JsonLint/ParsingException.php';
|
||||
|
|
|
@ -849,7 +849,7 @@ final class PhutilICSParser extends Phobject {
|
|||
);
|
||||
|
||||
// Load the map of Windows timezones.
|
||||
$root_path = dirname(phutil_get_library_root('phutil'));
|
||||
$root_path = dirname(phutil_get_library_root('arcanist'));
|
||||
$windows_path = $root_path.'/resources/timezones/windows_timezones.json';
|
||||
$windows_data = Filesystem::readFile($windows_path);
|
||||
$windows_zones = phutil_json_decode($windows_data);
|
||||
|
|
|
@ -59,7 +59,7 @@ final class PhagePHPAgentBootloader extends PhageAgentBootloader {
|
|||
);
|
||||
|
||||
$main_sequence = new PhutilBallOfPHP();
|
||||
$root = phutil_get_library_root('phutil');
|
||||
$root = phutil_get_library_root('arcanist');
|
||||
foreach ($files as $file) {
|
||||
$main_sequence->addFile($root.'/'.$file);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ final class PhutilSearchStemmer
|
|||
static $loaded;
|
||||
|
||||
if ($loaded === null) {
|
||||
$root = dirname(phutil_get_library_root('phutil'));
|
||||
$root = dirname(phutil_get_library_root('arcanist'));
|
||||
require_once $root.'/externals/porter-stemmer/src/Porter.php';
|
||||
$loaded = true;
|
||||
}
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Uses cscover (http://github.com/hach-que/cstools) to report code coverage.
|
||||
*
|
||||
* This engine inherits from `XUnitTestEngine`, where xUnit is used to actually
|
||||
* run the unit tests and this class provides a thin layer on top to collect
|
||||
* code coverage data with a third-party tool.
|
||||
*/
|
||||
final class CSharpToolsTestEngine extends XUnitTestEngine {
|
||||
|
||||
private $cscoverHintPath;
|
||||
private $coverEngine;
|
||||
private $cachedResults;
|
||||
private $matchRegex;
|
||||
private $excludedFiles;
|
||||
|
||||
/**
|
||||
* Overridden version of `loadEnvironment` to support a different set of
|
||||
* configuration values and to pull in the cstools config for code coverage.
|
||||
*/
|
||||
protected function loadEnvironment() {
|
||||
$config = $this->getConfigurationManager();
|
||||
$this->cscoverHintPath = $config->getConfigFromAnySource(
|
||||
'unit.csharp.cscover.binary');
|
||||
$this->matchRegex = $config->getConfigFromAnySource(
|
||||
'unit.csharp.coverage.match');
|
||||
$this->excludedFiles = $config->getConfigFromAnySource(
|
||||
'unit.csharp.coverage.excluded');
|
||||
|
||||
parent::loadEnvironment();
|
||||
|
||||
if ($this->getEnableCoverage() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine coverage path.
|
||||
if ($this->cscoverHintPath === null) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
"Unable to locate %s. Configure it with the '%s' option in %s.",
|
||||
'cscover',
|
||||
'unit.csharp.coverage.binary',
|
||||
'.arcconfig'));
|
||||
}
|
||||
$cscover = $this->projectRoot.DIRECTORY_SEPARATOR.$this->cscoverHintPath;
|
||||
if (file_exists($cscover)) {
|
||||
$this->coverEngine = Filesystem::resolvePath($cscover);
|
||||
} else {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unable to locate %s coverage runner (have you built yet?)',
|
||||
'cscover'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified assembly should be instrumented for
|
||||
* code coverage reporting. Checks the excluded file list and the
|
||||
* matching regex if they are configured.
|
||||
*
|
||||
* @return boolean Whether the assembly should be instrumented.
|
||||
*/
|
||||
private function assemblyShouldBeInstrumented($file) {
|
||||
if ($this->excludedFiles !== null) {
|
||||
if (array_key_exists((string)$file, $this->excludedFiles)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ($this->matchRegex !== null) {
|
||||
if (preg_match($this->matchRegex, $file) === 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden version of `buildTestFuture` so that the unit test can be run
|
||||
* via `cscover`, which instruments assemblies and reports on code coverage.
|
||||
*
|
||||
* @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) {
|
||||
if ($this->getEnableCoverage() === false) {
|
||||
return parent::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);
|
||||
}
|
||||
$cover_temp = new TempFile();
|
||||
$cover_temp->setPreserveFile(true);
|
||||
$xunit_cmd = $this->runtimeEngine;
|
||||
$xunit_args = null;
|
||||
if ($xunit_cmd === '') {
|
||||
$xunit_cmd = $this->testEngine;
|
||||
$xunit_args = csprintf(
|
||||
'%s /xml %s',
|
||||
$test_assembly,
|
||||
$xunit_temp);
|
||||
} else {
|
||||
$xunit_args = csprintf(
|
||||
'%s %s /xml %s',
|
||||
$this->testEngine,
|
||||
$test_assembly,
|
||||
$xunit_temp);
|
||||
}
|
||||
$assembly_dir = dirname($test_assembly);
|
||||
$assemblies_to_instrument = array();
|
||||
foreach (Filesystem::listDirectory($assembly_dir) as $file) {
|
||||
if (substr($file, -4) == '.dll' || substr($file, -4) == '.exe') {
|
||||
if ($this->assemblyShouldBeInstrumented($file)) {
|
||||
$assemblies_to_instrument[] = $assembly_dir.DIRECTORY_SEPARATOR.$file;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count($assemblies_to_instrument) === 0) {
|
||||
return parent::buildTestFuture($test_assembly);
|
||||
}
|
||||
$future = new ExecFuture(
|
||||
'%C -o %s -c %s -a %s -w %s %Ls',
|
||||
trim($this->runtimeEngine.' '.$this->coverEngine),
|
||||
$cover_temp,
|
||||
$xunit_cmd,
|
||||
$xunit_args,
|
||||
$assembly_dir,
|
||||
$assemblies_to_instrument);
|
||||
$future->setCWD(Filesystem::resolvePath($this->projectRoot));
|
||||
return array(
|
||||
$future,
|
||||
$assembly_dir.DIRECTORY_SEPARATOR.$xunit_temp,
|
||||
$cover_temp,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns coverage results for the unit tests.
|
||||
*
|
||||
* @param string The name of the coverage file if one was provided by
|
||||
* `buildTestFuture`.
|
||||
* @return array Code coverage results, or null.
|
||||
*/
|
||||
protected function parseCoverageResult($cover_file) {
|
||||
if ($this->getEnableCoverage() === false) {
|
||||
return parent::parseCoverageResult($cover_file);
|
||||
}
|
||||
return $this->readCoverage($cover_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cached results for a coverage result file. The coverage
|
||||
* result file is XML and can be large depending on what has been instrumented
|
||||
* so we cache it in case it's requested again.
|
||||
*
|
||||
* @param string The name of the coverage file.
|
||||
* @return array Code coverage results, or null if not cached.
|
||||
*/
|
||||
private function getCachedResultsIfPossible($cover_file) {
|
||||
if ($this->cachedResults == null) {
|
||||
$this->cachedResults = array();
|
||||
}
|
||||
if (array_key_exists((string)$cover_file, $this->cachedResults)) {
|
||||
return $this->cachedResults[(string)$cover_file];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the code coverage results in the cache.
|
||||
*
|
||||
* @param string The name of the coverage file.
|
||||
* @param array The results to cache.
|
||||
*/
|
||||
private function addCachedResults($cover_file, array $results) {
|
||||
if ($this->cachedResults == null) {
|
||||
$this->cachedResults = array();
|
||||
}
|
||||
$this->cachedResults[(string)$cover_file] = $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a set of XML tags as code coverage results. We parse
|
||||
* the `instrumented` and `executed` tags with this method so that
|
||||
* we can access the data multiple times without a performance hit.
|
||||
*
|
||||
* @param array The array of XML tags to parse.
|
||||
* @return array A PHP array containing the data.
|
||||
*/
|
||||
private function processTags($tags) {
|
||||
$results = array();
|
||||
foreach ($tags as $tag) {
|
||||
$results[] = array(
|
||||
'file' => $tag->getAttribute('file'),
|
||||
'start' => $tag->getAttribute('start'),
|
||||
'end' => $tag->getAttribute('end'),
|
||||
);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the code coverage results from the cscover results file.
|
||||
*
|
||||
* @param string The path to the code coverage file.
|
||||
* @return array The code coverage results.
|
||||
*/
|
||||
public function readCoverage($cover_file) {
|
||||
$cached = $this->getCachedResultsIfPossible($cover_file);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$coverage_dom = new DOMDocument();
|
||||
$coverage_dom->loadXML(Filesystem::readFile($cover_file));
|
||||
|
||||
$modified = $this->getPaths();
|
||||
$files = array();
|
||||
$reports = array();
|
||||
$instrumented = array();
|
||||
$executed = array();
|
||||
|
||||
$instrumented = $this->processTags(
|
||||
$coverage_dom->getElementsByTagName('instrumented'));
|
||||
$executed = $this->processTags(
|
||||
$coverage_dom->getElementsByTagName('executed'));
|
||||
|
||||
foreach ($instrumented as $instrument) {
|
||||
$absolute_file = $instrument['file'];
|
||||
$relative_file = substr($absolute_file, strlen($this->projectRoot) + 1);
|
||||
if (!in_array($relative_file, $files)) {
|
||||
$files[] = $relative_file;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$absolute_file = Filesystem::resolvePath(
|
||||
$this->projectRoot.DIRECTORY_SEPARATOR.$file);
|
||||
|
||||
// get total line count in file
|
||||
$line_count = count(file($absolute_file));
|
||||
|
||||
$coverage = array();
|
||||
for ($i = 0; $i < $line_count; $i++) {
|
||||
$coverage[$i] = 'N';
|
||||
}
|
||||
|
||||
foreach ($instrumented as $instrument) {
|
||||
if ($instrument['file'] !== $absolute_file) {
|
||||
continue;
|
||||
}
|
||||
for (
|
||||
$i = $instrument['start'];
|
||||
$i <= $instrument['end'];
|
||||
$i++) {
|
||||
$coverage[$i - 1] = 'U';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($executed as $execute) {
|
||||
if ($execute['file'] !== $absolute_file) {
|
||||
continue;
|
||||
}
|
||||
for (
|
||||
$i = $execute['start'];
|
||||
$i <= $execute['end'];
|
||||
$i++) {
|
||||
$coverage[$i - 1] = 'C';
|
||||
}
|
||||
}
|
||||
|
||||
$reports[$file] = implode($coverage);
|
||||
}
|
||||
|
||||
$this->addCachedResults($cover_file, $reports);
|
||||
return $reports;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Very basic 'nose' unit test engine wrapper.
|
||||
*
|
||||
* Requires nose 1.1.3 for code coverage.
|
||||
*/
|
||||
final class NoseTestEngine extends ArcanistUnitTestEngine {
|
||||
|
||||
private $parser;
|
||||
|
||||
protected function supportsRunAllTests() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function run() {
|
||||
if ($this->getRunAllTests()) {
|
||||
$root = $this->getWorkingCopy()->getProjectRoot();
|
||||
$all_tests = glob(Filesystem::resolvePath("$root/tests/**/test_*.py"));
|
||||
return $this->runTests($all_tests, $root);
|
||||
}
|
||||
|
||||
$paths = $this->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/<package>/test_<module>.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) {
|
||||
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_xml = Filesystem::readFile($cover_file);
|
||||
if (strlen($coverage_xml) < 1) {
|
||||
return array();
|
||||
}
|
||||
$coverage_dom = new DOMDocument();
|
||||
$coverage_dom->loadXML($coverage_xml);
|
||||
|
||||
$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 = (int)$line->getAttribute('number');
|
||||
for ($start_line; $start_line < $next_line; $start_line++) {
|
||||
$coverage .= 'N';
|
||||
}
|
||||
|
||||
if ((int)$line->getAttribute('hits') == 0) {
|
||||
$coverage .= 'U';
|
||||
} else if ((int)$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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,280 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPUnit wrapper.
|
||||
*/
|
||||
final class PhpunitTestEngine extends ArcanistUnitTestEngine {
|
||||
|
||||
private $configFile;
|
||||
private $phpunitBinary = 'phpunit';
|
||||
private $affectedTests;
|
||||
private $projectRoot;
|
||||
|
||||
public function run() {
|
||||
$this->projectRoot = $this->getWorkingCopy()->getProjectRoot();
|
||||
$this->affectedTests = array();
|
||||
foreach ($this->getPaths() as $path) {
|
||||
|
||||
$path = Filesystem::resolvePath($path, $this->projectRoot);
|
||||
|
||||
// TODO: add support for directories
|
||||
// Users can call phpunit on the directory themselves
|
||||
if (is_dir($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not sure if it would make sense to go further if
|
||||
// it is not a .php file
|
||||
if (substr($path, -4) != '.php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (substr($path, -8) == 'Test.php') {
|
||||
// Looks like a valid test file name.
|
||||
$this->affectedTests[$path] = $path;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($test = $this->findTestFile($path)) {
|
||||
$this->affectedTests[$path] = $test;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (empty($this->affectedTests)) {
|
||||
throw new ArcanistNoEffectException(pht('No tests to run.'));
|
||||
}
|
||||
|
||||
$this->prepareConfigFile();
|
||||
$futures = array();
|
||||
$tmpfiles = array();
|
||||
foreach ($this->affectedTests as $class_path => $test_path) {
|
||||
if (!Filesystem::pathExists($test_path)) {
|
||||
continue;
|
||||
}
|
||||
$json_tmp = new TempFile();
|
||||
$clover_tmp = null;
|
||||
$clover = null;
|
||||
if ($this->getEnableCoverage() !== false) {
|
||||
$clover_tmp = new TempFile();
|
||||
$clover = csprintf('--coverage-clover %s', $clover_tmp);
|
||||
}
|
||||
|
||||
$config = $this->configFile ? csprintf('-c %s', $this->configFile) : null;
|
||||
|
||||
$stderr = '-d display_errors=stderr';
|
||||
|
||||
$futures[$test_path] = new ExecFuture('%C %C %C --log-json %s %C %s',
|
||||
$this->phpunitBinary, $config, $stderr, $json_tmp, $clover, $test_path);
|
||||
$tmpfiles[$test_path] = array(
|
||||
'json' => $json_tmp,
|
||||
'clover' => $clover_tmp,
|
||||
);
|
||||
}
|
||||
|
||||
$results = array();
|
||||
$futures = id(new FutureIterator($futures))
|
||||
->limit(4);
|
||||
foreach ($futures as $test => $future) {
|
||||
|
||||
list($err, $stdout, $stderr) = $future->resolve();
|
||||
|
||||
$results[] = $this->parseTestResults(
|
||||
$test,
|
||||
$tmpfiles[$test]['json'],
|
||||
$tmpfiles[$test]['clover'],
|
||||
$stderr);
|
||||
}
|
||||
|
||||
return array_mergev($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse test results from phpunit json report.
|
||||
*
|
||||
* @param string $path Path to test
|
||||
* @param string $json_tmp Path to phpunit json report
|
||||
* @param string $clover_tmp Path to phpunit clover report
|
||||
* @param string $stderr Data written to stderr
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function parseTestResults($path, $json_tmp, $clover_tmp, $stderr) {
|
||||
$test_results = Filesystem::readFile($json_tmp);
|
||||
return id(new ArcanistPhpunitTestResultParser())
|
||||
->setEnableCoverage($this->getEnableCoverage())
|
||||
->setProjectRoot($this->projectRoot)
|
||||
->setCoverageFile($clover_tmp)
|
||||
->setAffectedTests($this->affectedTests)
|
||||
->setStderr($stderr)
|
||||
->parseTestResults($path, $test_results);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search for test cases for a given file in a large number of "reasonable"
|
||||
* locations. See @{method:getSearchLocationsForTests} for specifics.
|
||||
*
|
||||
* TODO: Add support for finding tests in testsuite folders from
|
||||
* phpunit.xml configuration.
|
||||
*
|
||||
* @param string PHP file to locate test cases for.
|
||||
* @return string|null Path to test cases, or null.
|
||||
*/
|
||||
private function findTestFile($path) {
|
||||
$root = $this->projectRoot;
|
||||
$path = Filesystem::resolvePath($path, $root);
|
||||
|
||||
$file = basename($path);
|
||||
$possible_files = array(
|
||||
$file,
|
||||
substr($file, 0, -4).'Test.php',
|
||||
);
|
||||
|
||||
$search = self::getSearchLocationsForTests($path);
|
||||
|
||||
foreach ($search as $search_path) {
|
||||
foreach ($possible_files as $possible_file) {
|
||||
$full_path = $search_path.$possible_file;
|
||||
if (!Filesystem::pathExists($full_path)) {
|
||||
// If the file doesn't exist, it's clearly a miss.
|
||||
continue;
|
||||
}
|
||||
if (!Filesystem::isDescendant($full_path, $root)) {
|
||||
// Don't look above the project root.
|
||||
continue;
|
||||
}
|
||||
if (0 == strcasecmp(Filesystem::resolvePath($full_path), $path)) {
|
||||
// Don't return the original file.
|
||||
continue;
|
||||
}
|
||||
return $full_path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get places to look for PHP Unit tests that cover a given file. For some
|
||||
* file "/a/b/c/X.php", we look in the same directory:
|
||||
*
|
||||
* /a/b/c/
|
||||
*
|
||||
* We then look in all parent directories for a directory named "tests/"
|
||||
* (or "Tests/"):
|
||||
*
|
||||
* /a/b/c/tests/
|
||||
* /a/b/tests/
|
||||
* /a/tests/
|
||||
* /tests/
|
||||
*
|
||||
* We also try to replace each directory component with "tests/":
|
||||
*
|
||||
* /a/b/tests/
|
||||
* /a/tests/c/
|
||||
* /tests/b/c/
|
||||
*
|
||||
* We also try to add "tests/" at each directory level:
|
||||
*
|
||||
* /a/b/c/tests/
|
||||
* /a/b/tests/c/
|
||||
* /a/tests/b/c/
|
||||
* /tests/a/b/c/
|
||||
*
|
||||
* This finds tests with a layout like:
|
||||
*
|
||||
* docs/
|
||||
* src/
|
||||
* tests/
|
||||
*
|
||||
* ...or similar. This list will be further pruned by the caller; it is
|
||||
* intentionally filesystem-agnostic to be unit testable.
|
||||
*
|
||||
* @param string PHP file to locate test cases for.
|
||||
* @return list<string> List of directories to search for tests in.
|
||||
*/
|
||||
public static function getSearchLocationsForTests($path) {
|
||||
$file = basename($path);
|
||||
$dir = dirname($path);
|
||||
|
||||
$test_dir_names = array('tests', 'Tests');
|
||||
|
||||
$try_directories = array();
|
||||
|
||||
// Try in the current directory.
|
||||
$try_directories[] = array($dir);
|
||||
|
||||
// Try in a tests/ directory anywhere in the ancestry.
|
||||
foreach (Filesystem::walkToRoot($dir) as $parent_dir) {
|
||||
if ($parent_dir == '/') {
|
||||
// We'll restore this later.
|
||||
$parent_dir = '';
|
||||
}
|
||||
foreach ($test_dir_names as $test_dir_name) {
|
||||
$try_directories[] = array($parent_dir, $test_dir_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Try replacing each directory component with 'tests/'.
|
||||
$parts = trim($dir, DIRECTORY_SEPARATOR);
|
||||
$parts = explode(DIRECTORY_SEPARATOR, $parts);
|
||||
foreach (array_reverse(array_keys($parts)) as $key) {
|
||||
foreach ($test_dir_names as $test_dir_name) {
|
||||
$try = $parts;
|
||||
$try[$key] = $test_dir_name;
|
||||
array_unshift($try, '');
|
||||
$try_directories[] = $try;
|
||||
}
|
||||
}
|
||||
|
||||
// Try adding 'tests/' at each level.
|
||||
foreach (array_reverse(array_keys($parts)) as $key) {
|
||||
foreach ($test_dir_names as $test_dir_name) {
|
||||
$try = $parts;
|
||||
$try[$key] = $test_dir_name.DIRECTORY_SEPARATOR.$try[$key];
|
||||
array_unshift($try, '');
|
||||
$try_directories[] = $try;
|
||||
}
|
||||
}
|
||||
|
||||
$results = array();
|
||||
foreach ($try_directories as $parts) {
|
||||
$results[implode(DIRECTORY_SEPARATOR, $parts).DIRECTORY_SEPARATOR] = true;
|
||||
}
|
||||
|
||||
return array_keys($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find and update phpunit configuration file based on
|
||||
* `phpunit_config` option in `.arcconfig`.
|
||||
*/
|
||||
private function prepareConfigFile() {
|
||||
$project_root = $this->projectRoot.DIRECTORY_SEPARATOR;
|
||||
$config = $this->getConfigurationManager()->getConfigFromAnySource(
|
||||
'phpunit_config');
|
||||
|
||||
if ($config) {
|
||||
if (Filesystem::pathExists($project_root.$config)) {
|
||||
$this->configFile = $project_root.$config;
|
||||
} else {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'PHPUnit configuration file was not found in %s',
|
||||
$project_root.$config));
|
||||
}
|
||||
}
|
||||
$bin = $this->getConfigurationManager()->getConfigFromAnySource(
|
||||
'unit.phpunit.binary');
|
||||
if ($bin) {
|
||||
if (Filesystem::binaryExists($bin)) {
|
||||
$this->phpunitBinary = $bin;
|
||||
} else {
|
||||
$this->phpunitBinary = Filesystem::resolvePath($bin, $project_root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Very basic 'py.test' unit test engine wrapper.
|
||||
*/
|
||||
final class PytestTestEngine extends ArcanistUnitTestEngine {
|
||||
|
||||
private $projectRoot;
|
||||
|
||||
public function run() {
|
||||
$working_copy = $this->getWorkingCopy();
|
||||
$this->projectRoot = $working_copy->getProjectRoot();
|
||||
|
||||
$junit_tmp = new TempFile();
|
||||
$cover_tmp = new TempFile();
|
||||
|
||||
$future = $this->buildTestFuture($junit_tmp, $cover_tmp);
|
||||
list($err, $stdout, $stderr) = $future->resolve();
|
||||
|
||||
if (!Filesystem::pathExists($junit_tmp)) {
|
||||
throw new CommandException(
|
||||
pht('Command failed with error #%s!', $err),
|
||||
$future->getCommand(),
|
||||
$err,
|
||||
$stdout,
|
||||
$stderr);
|
||||
}
|
||||
|
||||
$future = new ExecFuture('coverage xml -o %s', $cover_tmp);
|
||||
$future->setCWD($this->projectRoot);
|
||||
$future->resolvex();
|
||||
|
||||
return $this->parseTestResults($junit_tmp, $cover_tmp);
|
||||
}
|
||||
|
||||
public function buildTestFuture($junit_tmp, $cover_tmp) {
|
||||
$paths = $this->getPaths();
|
||||
|
||||
$cmd_line = csprintf('py.test --junit-xml=%s', $junit_tmp);
|
||||
|
||||
if ($this->getEnableCoverage() !== false) {
|
||||
$cmd_line = csprintf(
|
||||
'coverage run --source %s -m %C',
|
||||
$this->projectRoot,
|
||||
$cmd_line);
|
||||
}
|
||||
|
||||
return new ExecFuture('%C', $cmd_line);
|
||||
}
|
||||
|
||||
public function parseTestResults($junit_tmp, $cover_tmp) {
|
||||
$parser = new ArcanistXUnitTestResultParser();
|
||||
$results = $parser->parseTestResults(
|
||||
Filesystem::readFile($junit_tmp));
|
||||
|
||||
if ($this->getEnableCoverage() !== false) {
|
||||
$coverage_report = $this->readCoverage($cover_tmp);
|
||||
foreach ($results as $result) {
|
||||
$result->setCoverage($coverage_report);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function readCoverage($path) {
|
||||
$coverage_data = Filesystem::readFile($path);
|
||||
if (empty($coverage_data)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$coverage_dom = new DOMDocument();
|
||||
$coverage_dom->loadXML($coverage_data);
|
||||
|
||||
$paths = $this->getPaths();
|
||||
$reports = array();
|
||||
$classes = $coverage_dom->getElementsByTagName('class');
|
||||
|
||||
foreach ($classes as $class) {
|
||||
// filename is actually python module path with ".py" at the end,
|
||||
// e.g.: tornado.web.py
|
||||
$relative_path = explode('.', $class->getAttribute('filename'));
|
||||
array_pop($relative_path);
|
||||
$relative_path = implode('/', $relative_path);
|
||||
|
||||
// first we check if the path is a directory (a Python package), if it is
|
||||
// set relative and absolute paths to have __init__.py at the end.
|
||||
$absolute_path = Filesystem::resolvePath($relative_path);
|
||||
if (is_dir($absolute_path)) {
|
||||
$relative_path .= '/__init__.py';
|
||||
$absolute_path .= '/__init__.py';
|
||||
}
|
||||
|
||||
// then we check if the path with ".py" at the end is file (a Python
|
||||
// submodule), if it is - set relative and absolute paths to have
|
||||
// ".py" at the end.
|
||||
if (is_file($absolute_path.'.py')) {
|
||||
$relative_path .= '.py';
|
||||
$absolute_path .= '.py';
|
||||
}
|
||||
|
||||
if (!file_exists($absolute_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($relative_path, $paths)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get total line count in file
|
||||
$line_count = count(file($absolute_path));
|
||||
|
||||
$coverage = '';
|
||||
$start_line = 1;
|
||||
$lines = $class->getElementsByTagName('line');
|
||||
for ($ii = 0; $ii < $lines->length; $ii++) {
|
||||
$line = $lines->item($ii);
|
||||
|
||||
$next_line = (int)$line->getAttribute('number');
|
||||
for ($start_line; $start_line < $next_line; $start_line++) {
|
||||
$coverage .= 'N';
|
||||
}
|
||||
|
||||
if ((int)$line->getAttribute('hits') == 0) {
|
||||
$coverage .= 'U';
|
||||
} else if ((int)$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[$relative_path] = $coverage;
|
||||
}
|
||||
|
||||
return $reports;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,465 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Uses xUnit (http://xunit.codeplex.com/) to test C# code.
|
||||
*
|
||||
* Assumes that when modifying a file with a path like `SomeAssembly/MyFile.cs`,
|
||||
* that the test assembly that verifies the functionality of `SomeAssembly` is
|
||||
* located at `SomeAssembly.Tests`.
|
||||
*
|
||||
* @concrete-extensible
|
||||
*/
|
||||
class XUnitTestEngine extends ArcanistUnitTestEngine {
|
||||
|
||||
protected $runtimeEngine;
|
||||
protected $buildEngine;
|
||||
protected $testEngine;
|
||||
protected $projectRoot;
|
||||
protected $xunitHintPath;
|
||||
protected $discoveryRules;
|
||||
|
||||
/**
|
||||
* This test engine supports running all tests.
|
||||
*/
|
||||
protected function supportsRunAllTests() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what executables and test paths to use. Between platforms this
|
||||
* also changes whether the test engine is run under .NET or Mono. It also
|
||||
* ensures that all of the required binaries are available for the tests to
|
||||
* run successfully.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function loadEnvironment() {
|
||||
$this->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) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Tests for @{class:PhpunitTestEngine}.
|
||||
*/
|
||||
final class PhpunitTestEngineTestCase extends PhutilTestCase {
|
||||
|
||||
public function testSearchLocations() {
|
||||
$path = '/path/to/some/file/X.php';
|
||||
|
||||
$this->assertEqual(
|
||||
array(
|
||||
'/path/to/some/file/',
|
||||
'/path/to/some/file/tests/',
|
||||
'/path/to/some/file/Tests/',
|
||||
'/path/to/some/tests/',
|
||||
'/path/to/some/Tests/',
|
||||
'/path/to/tests/',
|
||||
'/path/to/Tests/',
|
||||
'/path/tests/',
|
||||
'/path/Tests/',
|
||||
'/tests/',
|
||||
'/Tests/',
|
||||
'/path/to/tests/file/',
|
||||
'/path/to/Tests/file/',
|
||||
'/path/tests/some/file/',
|
||||
'/path/Tests/some/file/',
|
||||
'/tests/to/some/file/',
|
||||
'/Tests/to/some/file/',
|
||||
'/path/to/some/tests/file/',
|
||||
'/path/to/some/Tests/file/',
|
||||
'/path/to/tests/some/file/',
|
||||
'/path/to/Tests/some/file/',
|
||||
'/path/tests/to/some/file/',
|
||||
'/path/Tests/to/some/file/',
|
||||
'/tests/path/to/some/file/',
|
||||
'/Tests/path/to/some/file/',
|
||||
),
|
||||
PhpunitTestEngine::getSearchLocationsForTests($path));
|
||||
}
|
||||
|
||||
}
|
|
@ -120,6 +120,9 @@ final class PhutilUnitTestEngineTestCase extends PhutilTestCase {
|
|||
}
|
||||
|
||||
public function testGetTestPaths() {
|
||||
|
||||
$this->assertSkipped(pht('TOOLSETS: No test path selection yet.'));
|
||||
|
||||
$tests = array(
|
||||
'empty' => array(
|
||||
array(),
|
||||
|
|
|
@ -538,6 +538,7 @@ final class PhutilUtilsTestCase extends PhutilTestCase {
|
|||
} catch (Exception $ex) {
|
||||
$caught = $ex;
|
||||
}
|
||||
|
||||
$this->assertTrue($caught instanceof PhutilJSONParserException);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
$arcanist_root = dirname(dirname(dirname(__FILE__)));
|
||||
require_once $arcanist_root.'/scripts/init/init-script.php';
|
||||
|
||||
$logs = array();
|
||||
for ($ii = 0; $ii < $argv[1]; $ii++) {
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
$arcanist_root = dirname(dirname(dirname(__FILE__)));
|
||||
require_once $arcanist_root.'/scripts/init/init-script.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('acquire and hold a lockfile'));
|
Loading…
Reference in a new issue