1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-29 10:12:41 +01:00

Fix issues in C# unit test engine

Summary: This fixes a few issues in the C# unit test engine.  It fixes tests sitting in subdirectories not being tested correctly (the location of both the test assembly and the results file would be wrong).  It also fixes a very strange issue where xUnit.NET seems to not output the resulting XML file when it executes; in this case we just retry running the test until the XML file appears after completion (and eventually it works).

Test Plan: Ran `arc unit --everything` and `arc unit --everything --no-coverage` and verified that it's all reliably working.

Reviewers: epriestley, #blessed_reviewers, hach-que

Reviewed By: epriestley

CC: Korvin, epriestley, aran

Differential Revision: https://secure.phabricator.com/D7594
This commit is contained in:
James Rhodes 2013-11-22 14:32:48 -08:00 committed by epriestley
parent 6033f14221
commit bd191c2860
2 changed files with 100 additions and 92 deletions

View file

@ -128,6 +128,9 @@ final class CSharpToolsTestEngine extends XUnitTestEngine {
} }
} }
} }
if (count($assemblies_to_instrument) === 0) {
return parent::buildTestFuture($test_assembly);
}
$future = new ExecFuture( $future = new ExecFuture(
"%C -o %s -c %s -a %s -w %s %Ls", "%C -o %s -c %s -a %s -w %s %Ls",
trim($this->runtimeEngine." ".$this->coverEngine), trim($this->runtimeEngine." ".$this->coverEngine),

View file

@ -17,6 +17,7 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
protected $testEngine; protected $testEngine;
protected $projectRoot; protected $projectRoot;
protected $xunitHintPath; protected $xunitHintPath;
protected $discoveryRules;
/** /**
* This test engine supports running all tests. * This test engine supports running all tests.
@ -54,16 +55,24 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
throw new Exception("Unable to find Mono and you are not on Windows!"); throw new Exception("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(
"You must configure discovery rules to map C# files ".
"back to test projects (`unit.csharp.discovery` in .arcconfig).");
}
// Determine xUnit test runner path. // Determine xUnit test runner path.
if ($this->xunitHintPath === null) { if ($this->xunitHintPath === null) {
$this->xunitHintPath = $this->xunitHintPath =
$this->getConfigurationManager()->getConfigFromAnySource( $this->getConfigurationManager()->getConfigFromAnySource(
'unit.xunit.binary'); 'unit.csharp.xunit.binary');
} }
if ($this->xunitHintPath === null) { $xunit = $this->projectRoot.DIRECTORY_SEPARATOR.$this->xunitHintPath;
} if (file_exists($xunit) && $this->xunitHintPath !== null) {
$xunit = $this->projectRoot."/".$this->xunitHintPath;
if (file_exists($xunit)) {
$this->testEngine = Filesystem::resolvePath($xunit); $this->testEngine = Filesystem::resolvePath($xunit);
} else if (Filesystem::binaryExists("xunit.console.clr4.exe")) { } else if (Filesystem::binaryExists("xunit.console.clr4.exe")) {
$this->testEngine = "xunit.console.clr4.exe"; $this->testEngine = "xunit.console.clr4.exe";
@ -74,39 +83,6 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
} }
} }
/**
* Returns all available tests and related projects. Recurses into
* Protobuild submodules if they are present.
*
* @return array Mappings of test project to project being tested.
*/
public function getAllAvailableTestsAndRelatedProjects($path = null) {
if ($path == null) {
$path = $this->projectRoot;
}
$entries = Filesystem::listDirectory($path);
$mappings = array();
foreach ($entries as $entry) {
if (substr($entry, -6) === ".Tests") {
if (is_dir($path."/".$entry)) {
$mappings[$path."/".$entry] = $path."/".
substr($entry, 0, strlen($entry) - 6);
}
} elseif (is_dir($path."/".$entry."/Build")) {
if (file_exists($path."/".$entry."/Build/Module.xml")) {
// The entry is a Protobuild submodule, which we should
// also recurse into.
$submappings =
$this->getAllAvailableTestsAndRelatedProjects($path."/".$entry);
foreach ($submappings as $key => $value) {
$mappings[$key] = $value;
}
}
}
}
return $mappings;
}
/** /**
* Main entry point for the test engine. Determines what assemblies to * Main entry point for the test engine. Determines what assemblies to
* build and test based on the files that have changed. * build and test based on the files that have changed.
@ -117,56 +93,69 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
$this->loadEnvironment(); $this->loadEnvironment();
$affected_tests = array();
if ($this->getRunAllTests()) { if ($this->getRunAllTests()) {
echo "Loading tests..."."\n"; $paths = id(new FileFinder($this->projectRoot))->find();
$entries = $this->getAllAvailableTestsAndRelatedProjects();
foreach ($entries as $key => $value) {
echo "Test: ".substr($key, strlen($this->projectRoot) + 1)."\n";
$affected_tests[] = substr($key, strlen($this->projectRoot) + 1);
}
} else { } else {
$paths = $this->getPaths(); $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) { foreach ($paths as $path) {
if (substr($path, -4) == ".dll" || if (preg_match('/'.$regex.'/', $path) === 1) {
substr($path, -4) == ".mdb") { foreach ($targets as $target) {
continue; // Index 0 is the test project (.csproj file)
} // Index 1 is the output assembly (.dll file)
if (substr_count($path, "/") > 0) { $project = preg_replace('/'.$regex.'/', $target[0], $path);
$components = explode("/", $path); $project = $this->projectRoot.DIRECTORY_SEPARATOR.$project;
$affected_assembly = $components[0]; $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);
// If the change is made inside an assembly that has a `.Tests` // Check to ensure uniqueness.
// extension, then the developer has changed the actual tests. $exists = false;
if (substr($affected_assembly, -6) === ".Tests") { foreach ($results as $existing) {
$affected_assembly_path = Filesystem::resolvePath( if ($existing['assembly'] === $assembly) {
$affected_assembly); $exists = true;
$test_assembly = $affected_assembly; break;
} else { }
$affected_assembly_path = Filesystem::resolvePath( }
$affected_assembly.".Tests");
$test_assembly = $affected_assembly.".Tests"; if (!$exists) {
} print "Discovered test at ".$assembly."\n";
if (is_dir($affected_assembly_path) && $results[] = array(
!in_array($test_assembly, $affected_tests)) { 'project' => $project,
$affected_tests[] = $test_assembly; 'assembly' => $assembly);
}
}
} }
} }
} }
} }
return $results;
return $this->runAllTests($affected_tests);
} }
/** /**
* Builds and runs the specified test assemblies. * Builds and runs the specified test assemblies.
* *
* @param array Array of test assemblies. * @param array Array of paths to test project files.
* @return array Array of test results. * @return array Array of test results.
*/ */
public function runAllTests(array $test_assemblies) { public function runAllTests(array $test_projects) {
if (empty($test_assemblies)) { if (empty($test_projects)) {
return array(); return array();
} }
@ -175,11 +164,11 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
if ($this->resultsContainFailures($results)) { if ($this->resultsContainFailures($results)) {
return array_mergev($results); return array_mergev($results);
} }
$results[] = $this->buildProjects($test_assemblies); $results[] = $this->buildProjects($test_projects);
if ($this->resultsContainFailures($results)) { if ($this->resultsContainFailures($results)) {
return array_mergev($results); return array_mergev($results);
} }
$results[] = $this->testAssemblies($test_assemblies); $results[] = $this->testAssemblies($test_projects);
return array_mergev($results); return array_mergev($results);
} }
@ -220,6 +209,13 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
return array(); 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. // Work out what platform the user is building for already.
$platform = phutil_is_windows() ? "Windows" : "Linux"; $platform = phutil_is_windows() ? "Windows" : "Linux";
$files = Filesystem::listDirectory($this->projectRoot); $files = Filesystem::listDirectory($this->projectRoot);
@ -280,8 +276,8 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
$this->buildEngine, $this->buildEngine,
"/p:SkipTestsOnBuild=True"); "/p:SkipTestsOnBuild=True");
$build_future->setCWD(Filesystem::resolvePath( $build_future->setCWD(Filesystem::resolvePath(
$this->projectRoot."/".$test_assembly)); dirname($test_assembly['project'])));
$build_futures[$test_assembly] = $build_future; $build_futures[$test_assembly['project']] = $build_future;
} }
$iterator = Futures($build_futures)->limit(1); $iterator = Futures($build_futures)->limit(1);
foreach ($iterator as $test_assembly => $future) { foreach ($iterator as $test_assembly => $future) {
@ -319,17 +315,22 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
// FIXME: Can't use TempFile here as xUnit doesn't like // FIXME: Can't use TempFile here as xUnit doesn't like
// UNIX-style full paths. It sees the leading / as the // UNIX-style full paths. It sees the leading / as the
// start of an option flag, even when quoted. // start of an option flag, even when quoted.
$xunit_temp = $test_assembly.".results.xml"; $xunit_temp = Filesystem::readRandomCharacters(10).".results.xml";
if (file_exists($xunit_temp)) { if (file_exists($xunit_temp)) {
unlink($xunit_temp); unlink($xunit_temp);
} }
$future = new ExecFuture( $future = new ExecFuture(
"%C %s /xml %s /silent", "%C %s /xml %s",
trim($this->runtimeEngine." ".$this->testEngine), trim($this->runtimeEngine." ".$this->testEngine),
$test_assembly."/bin/Debug/".$test_assembly.".dll", $test_assembly,
$xunit_temp); $xunit_temp);
$future->setCWD(Filesystem::resolvePath($this->projectRoot)); $folder = Filesystem::resolvePath($this->projectRoot);
return array($future, $xunit_temp, null); $future->setCWD($folder);
$combined = $folder."/".$xunit_temp;
if (phutil_is_windows()) {
$combined = $folder."\\".$xunit_temp;
}
return array($future, $combined, null);
} }
/** /**
@ -348,16 +349,16 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
$outputs = array(); $outputs = array();
$coverages = array(); $coverages = array();
foreach ($test_assemblies as $test_assembly) { foreach ($test_assemblies as $test_assembly) {
list($future, $xunit_temp, $coverage) = list($future_r, $xunit_temp, $coverage) =
$this->buildTestFuture($test_assembly); $this->buildTestFuture($test_assembly['assembly']);
$futures[$test_assembly] = $future; $futures[$test_assembly['assembly']] = $future_r;
$outputs[$test_assembly] = $xunit_temp; $outputs[$test_assembly['assembly']] = $xunit_temp;
$coverages[$test_assembly] = $coverage; $coverages[$test_assembly['assembly']] = $coverage;
} }
// Run all of the tests. // Run all of the tests.
foreach (Futures($futures) as $test_assembly => $future) { foreach (Futures($futures)->limit(8) as $test_assembly => $future) {
$future->resolve(); list($err, $stdout, $stderr) = $future->resolve();
if (file_exists($outputs[$test_assembly])) { if (file_exists($outputs[$test_assembly])) {
$result = $this->parseTestResult( $result = $this->parseTestResult(
@ -366,11 +367,15 @@ class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
$results[] = $result; $results[] = $result;
unlink($outputs[$test_assembly]); unlink($outputs[$test_assembly]);
} else { } else {
$result = new ArcanistUnitTestResult(); // FIXME: There's a bug in Mono which causes a segmentation fault
$result->setName("(execute) ".$test_assembly); // when xUnit.NET runs; this causes the XML file to not appear
$result->setResult(ArcanistUnitTestResult::RESULT_BROKEN); // (depending on when the segmentation fault occurs). See
$result->setUserData($outputs[$test_assembly]." not found on disk."); // https://bugzilla.xamarin.com/show_bug.cgi?id=16379
$results[] = array($result); // 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.
//
} }
} }