Changeset View
Changeset View
Standalone View
Standalone View
src/unit/engine/XUnitTestEngine.php
<?php | <?php | ||||
/** | /** | ||||
* Uses xUnit (http://xunit.codeplex.com/) to test C# code. | * Uses xUnit (http://xunit.codeplex.com/) to test C# code. | ||||
* | * | ||||
* Assumes that when modifying a file with a path like `SomeAssembly/MyFile.cs`, | * Assumes that when modifying a file with a path like `SomeAssembly/MyFile.cs`, | ||||
* that the test assembly that verifies the functionality of `SomeAssembly` is | * that the test assembly that verifies the functionality of `SomeAssembly` is | ||||
* located at `SomeAssembly.Tests`. | * located at `SomeAssembly.Tests`. | ||||
* | * | ||||
* @group unitrun | |||||
* @concrete-extensible | * @concrete-extensible | ||||
*/ | */ | ||||
class XUnitTestEngine extends ArcanistBaseUnitTestEngine { | class XUnitTestEngine extends ArcanistBaseUnitTestEngine { | ||||
protected $runtimeEngine; | protected $runtimeEngine; | ||||
protected $buildEngine; | protected $buildEngine; | ||||
protected $testEngine; | protected $testEngine; | ||||
protected $projectRoot; | protected $projectRoot; | ||||
protected $xunitHintPath; | protected $xunitHintPath; | ||||
protected $discoveryRules; | protected $discoveryRules; | ||||
/** | /** | ||||
* This test engine supports running all tests. | * This test engine supports running all tests. | ||||
*/ | */ | ||||
protected function supportsRunAllTests() { | protected function supportsRunAllTests() { | ||||
return true; | return true; | ||||
} | } | ||||
/** | /** | ||||
* Determines what executables and test paths to use. Between platforms | * Determines what executables and test paths to use. Between platforms this | ||||
* this also changes whether the test engine is run under .NET or Mono. It | * also changes whether the test engine is run under .NET or Mono. It also | ||||
* also ensures that all of the required binaries are available for the tests | * ensures that all of the required binaries are available for the tests to | ||||
* to run successfully. | * run successfully. | ||||
* | * | ||||
* @return void | * @return void | ||||
*/ | */ | ||||
protected function loadEnvironment() { | protected function loadEnvironment() { | ||||
$this->projectRoot = $this->getWorkingCopy()->getProjectRoot(); | $this->projectRoot = $this->getWorkingCopy()->getProjectRoot(); | ||||
// Determine build engine. | // Determine build engine. | ||||
if (Filesystem::binaryExists('msbuild')) { | if (Filesystem::binaryExists('msbuild')) { | ||||
Show All 31 Lines | protected function loadEnvironment() { | ||||
} | } | ||||
$xunit = $this->projectRoot.DIRECTORY_SEPARATOR.$this->xunitHintPath; | $xunit = $this->projectRoot.DIRECTORY_SEPARATOR.$this->xunitHintPath; | ||||
if (file_exists($xunit) && $this->xunitHintPath !== null) { | if (file_exists($xunit) && $this->xunitHintPath !== null) { | ||||
$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'; | ||||
} else { | } else { | ||||
throw new Exception( | throw new Exception( | ||||
"Unable to locate xUnit console runner. Configure ". | "Unable to locate xUnit console runner. Configure ". | ||||
"it with the `unit.csharp.xunit.binary' option in .arcconfig"); | "it with the `unit.csharp.xunit.binary' option in .arcconfig"); | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* Main entry point for the test engine. Determines what assemblies to | * Main entry point for the test engine. Determines what assemblies to build | ||||
* build and test based on the files that have changed. | * and test based on the files that have changed. | ||||
* | * | ||||
* @return array Array of test results. | * @return array Array of test results. | ||||
*/ | */ | ||||
public function run() { | public function run() { | ||||
$this->loadEnvironment(); | $this->loadEnvironment(); | ||||
if ($this->getRunAllTests()) { | if ($this->getRunAllTests()) { | ||||
Show All 35 Lines | foreach ($this->discoveryRules as $regex => $targets) { | ||||
$exists = true; | $exists = true; | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
if (!$exists) { | if (!$exists) { | ||||
$results[] = array( | $results[] = array( | ||||
'project' => $project, | 'project' => $project, | ||||
'assembly' => $assembly); | 'assembly' => $assembly, | ||||
); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
return $results; | return $results; | ||||
} | } | ||||
Show All 38 Lines | foreach ($results as $result) { | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
return false; | return false; | ||||
} | } | ||||
/** | /** | ||||
* If the `Build` directory exists, we assume that this is a multi-platform | * 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 | * project that requires generation of C# project files. Because we want to | ||||
* test that the generation and subsequent build is whole, we need 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 | * regenerate any projects in case the developer has added files through an | ||||
* IDE and then forgotten to add them to the respective `.definitions` file. | * IDE and then forgotten to add them to the respective `.definitions` file. | ||||
* By regenerating the projects we ensure that any missing definition entries | * By regenerating the projects we ensure that any missing definition entries | ||||
* will cause the build to fail. | * will cause the build to fail. | ||||
* | * | ||||
* @return array Array of test results. | * @return array Array of test results. | ||||
*/ | */ | ||||
private function generateProjects() { | private function generateProjects() { | ||||
// No "Build" directory; so skip generation of projects. | // No "Build" directory; so skip generation of projects. | ||||
if (!is_dir(Filesystem::resolvePath($this->projectRoot.'/Build'))) { | if (!is_dir(Filesystem::resolvePath($this->projectRoot.'/Build'))) { | ||||
return array(); | return array(); | ||||
} | } | ||||
// No "Protobuild.exe" file; so skip generation of projects. | // No "Protobuild.exe" file; so skip generation of projects. | ||||
if (!is_file(Filesystem::resolvePath( | if (!is_file(Filesystem::resolvePath( | ||||
$this->projectRoot.'/Protobuild.exe'))) { | $this->projectRoot.'/Protobuild.exe'))) { | ||||
Show All 36 Lines | private function generateProjects() { | ||||
$result->setDuration(microtime(true) - $regenerate_start); | $result->setDuration(microtime(true) - $regenerate_start); | ||||
$results[] = $result; | $results[] = $result; | ||||
return $results; | return $results; | ||||
} | } | ||||
/** | /** | ||||
* Build the projects relevant for the specified test assemblies and return | * Build the projects relevant for the specified test assemblies and return | ||||
* the results of the builds as test results. This build also passes the | * the results of the builds as test results. This build also passes the | ||||
* "SkipTestsOnBuild" parameter when building the projects, so that MSBuild | * "SkipTestsOnBuild" parameter when building the projects, so that MSBuild | ||||
* conditionals can be used to prevent any tests running as part of the | * 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 | * build itself (since the unit tester is about to run each of the tests | ||||
* individually). | * individually). | ||||
* | * | ||||
* @param array Array of test assemblies. | * @param array Array of test assemblies. | ||||
* @return array Array of test results. | * @return array Array of test results. | ||||
*/ | */ | ||||
Show All 30 Lines | foreach ($iterator as $test_assembly => $future) { | ||||
$result->setDuration(microtime(true) - $build_start); | $result->setDuration(microtime(true) - $build_start); | ||||
$results[] = $result; | $results[] = $result; | ||||
} | } | ||||
return $results; | return $results; | ||||
} | } | ||||
/** | /** | ||||
* Build the future for running a unit test. This can be | * Build the future for running a unit test. This can be overridden to enable | ||||
* overridden to enable support for code coverage via | * support for code coverage via another tool. | ||||
* another tool | |||||
* | * | ||||
* @param string Name of the test assembly. | * @param string Name of the test assembly. | ||||
* @return array The future, output filename and coverage filename | * @return array The future, output filename and coverage filename | ||||
* stored in an array. | * stored in an array. | ||||
*/ | */ | ||||
protected function buildTestFuture($test_assembly) { | protected function buildTestFuture($test_assembly) { | ||||
// 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 = Filesystem::readRandomCharacters(10).'.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', | '%C %s /xml %s', | ||||
trim($this->runtimeEngine.' '.$this->testEngine), | trim($this->runtimeEngine.' '.$this->testEngine), | ||||
Show All 11 Lines | class XUnitTestEngine extends ArcanistBaseUnitTestEngine { | ||||
/** | /** | ||||
* Run the xUnit test runner on each of the assemblies and parse the | * Run the xUnit test runner on each of the assemblies and parse the | ||||
* resulting XML. | * resulting XML. | ||||
* | * | ||||
* @param array Array of test assemblies. | * @param array Array of test assemblies. | ||||
* @return array Array of test results. | * @return array Array of test results. | ||||
*/ | */ | ||||
private function testAssemblies(array $test_assemblies) { | private function testAssemblies(array $test_assemblies) { | ||||
$results = array(); | $results = array(); | ||||
// Build the futures for running the tests. | // Build the futures for running the tests. | ||||
$futures = array(); | $futures = array(); | ||||
$outputs = array(); | $outputs = array(); | ||||
$coverages = array(); | $coverages = array(); | ||||
foreach ($test_assemblies as $test_assembly) { | foreach ($test_assemblies as $test_assembly) { | ||||
list($future_r, $xunit_temp, $coverage) = | list($future_r, $xunit_temp, $coverage) = | ||||
Show All 11 Lines | foreach (Futures($futures)->limit(8) as $test_assembly => $future) { | ||||
$result = $this->parseTestResult( | $result = $this->parseTestResult( | ||||
$outputs[$test_assembly], | $outputs[$test_assembly], | ||||
$coverages[$test_assembly]); | $coverages[$test_assembly]); | ||||
$results[] = $result; | $results[] = $result; | ||||
unlink($outputs[$test_assembly]); | unlink($outputs[$test_assembly]); | ||||
} else { | } else { | ||||
// FIXME: There's a bug in Mono which causes a segmentation fault | // FIXME: There's a bug in Mono which causes a segmentation fault | ||||
// when xUnit.NET runs; this causes the XML file to not appear | // when xUnit.NET runs; this causes the XML file to not appear | ||||
// (depending on when the segmentation fault occurs). See | // (depending on when the segmentation fault occurs). See | ||||
// https://bugzilla.xamarin.com/show_bug.cgi?id=16379 | // https://bugzilla.xamarin.com/show_bug.cgi?id=16379 | ||||
// for more information. | // for more information. | ||||
// Since it's not possible for the user to correct this error, we | // Since it's not possible for the user to correct this error, we | ||||
// ignore the fact the tests didn't run here. | // ignore the fact the tests didn't run here. | ||||
// | |||||
} | } | ||||
} | } | ||||
return array_mergev($results); | return array_mergev($results); | ||||
} | } | ||||
/** | /** | ||||
* Returns null for this implementation as xUnit does not support code | * Returns null for this implementation as xUnit does not support code | ||||
* coverage directly. Override this method in another class to provide | * coverage directly. Override this method in another class to provide code | ||||
* code coverage information (also see `CSharpToolsUnitEngine`). | * coverage information (also see @{class:CSharpToolsUnitEngine}). | ||||
* | * | ||||
* @param string The name of the coverage file if one was provided by | * @param string The name of the coverage file if one was provided by | ||||
* `buildTestFuture`. | * `buildTestFuture`. | ||||
* @return array Code coverage results, or null. | * @return array Code coverage results, or null. | ||||
*/ | */ | ||||
protected function parseCoverageResult($coverage) { | protected function parseCoverageResult($coverage) { | ||||
return null; | return null; | ||||
} | } | ||||
/** | /** | ||||
* Parses the test results from xUnit. | * Parses the test results from xUnit. | ||||
* | * | ||||
* @param string The name of the xUnit results file. | * @param string The name of the xUnit results file. | ||||
* @param string The name of the coverage file if one was provided by | * @param string The name of the coverage file if one was provided by | ||||
* `buildTestFuture`. This is passed through to | * `buildTestFuture`. This is passed through to | ||||
* `parseCoverageResult`. | * `parseCoverageResult`. | ||||
* @return array Test results. | * @return array Test results. | ||||
*/ | */ | ||||
private function parseTestResult($xunit_tmp, $coverage) { | private function parseTestResult($xunit_tmp, $coverage) { | ||||
$xunit_dom = new DOMDocument(); | $xunit_dom = new DOMDocument(); | ||||
$xunit_dom->loadXML(Filesystem::readFile($xunit_tmp)); | $xunit_dom->loadXML(Filesystem::readFile($xunit_tmp)); | ||||
$results = array(); | $results = array(); | ||||
▲ Show 20 Lines • Show All 46 Lines • Show Last 20 Lines |