Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F969312
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/unit/engine/CSharpToolsTestEngine.php b/src/unit/engine/CSharpToolsTestEngine.php
index 34f8e511..9b85257a 100644
--- a/src/unit/engine/CSharpToolsTestEngine.php
+++ b/src/unit/engine/CSharpToolsTestEngine.php
@@ -1,290 +1,288 @@
<?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.
*
* @group unitrun
*/
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_item = 'unit.csharp.xunit.binary') {
+ protected function loadEnvironment() {
$config = $this->getConfigurationManager();
- $this->xunitHintPath =
- $config->getConfigFromAnySource('unit.csharp.xunit.binary');
$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($config_item);
+ parent::loadEnvironment();
if ($this->getEnableCoverage() === false) {
return;
}
// Determine coverage path.
if ($this->cscoverHintPath === null) {
throw new Exception(
"Unable to locate cscover. Configure it with ".
"the `unit.csharp.coverage.binary' option in .arcconfig");
}
- $cscover = $this->projectRoot."/".$this->cscoverHintPath;
+ $cscover = $this->projectRoot.DIRECTORY_SEPARATOR.$this->cscoverHintPath;
if (file_exists($cscover)) {
$this->coverEngine = Filesystem::resolvePath($cscover);
} else {
throw new Exception(
"Unable to locate cscover coverage runner ".
"(have you built yet?)");
}
}
/**
* 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 = $test_assembly.".results.xml";
+ $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 /silent",
- $test_assembly.".dll",
+ "%s /xml %s",
+ $test_assembly,
$xunit_temp);
} else {
$xunit_args = csprintf(
- "%s %s /xml %s /silent",
+ "%s %s /xml %s",
$this->testEngine,
- $test_assembly.".dll",
+ $test_assembly,
$xunit_temp);
}
- $assembly_dir = $test_assembly."/bin/Debug/";
+ $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.$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,
- $this->projectRoot."/".$assembly_dir.$xunit_temp,
+ $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 = $this->projectRoot."/".$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;
}
}
diff --git a/src/unit/engine/XUnitTestEngine.php b/src/unit/engine/XUnitTestEngine.php
index db18bbef..00338244 100644
--- a/src/unit/engine/XUnitTestEngine.php
+++ b/src/unit/engine/XUnitTestEngine.php
@@ -1,456 +1,456 @@
<?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`.
*
* @group unitrun
* @concrete-extensible
*/
class XUnitTestEngine extends ArcanistBaseUnitTestEngine {
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($config_item = 'unit.xunit.binary') {
+ 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("Unable to find msbuild or xbuild in 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("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.
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(
"Unable to locate xUnit console runner. Configure ".
- "it with the `$config_item' option in .arcconfig");
+ "it with the `unit.csharp.xunit.binary' option in .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("(regenerate projects for $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 = Futures($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.
foreach (Futures($futures)->limit(8) 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 `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;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 22, 13:12 (12 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
547708
Default Alt Text
(25 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment