mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-15 03:12:40 +01:00
282 lines
8.4 KiB
PHP
282 lines
8.4 KiB
PHP
|
<?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() {
|
||
|
$working = $this->getWorkingCopy();
|
||
|
$this->xunitHintPath = $working->getConfig('unit.csharp.xunit.binary');
|
||
|
$this->cscoverHintPath = $working->getConfig('unit.csharp.cscover.binary');
|
||
|
$this->matchRegex = $working->getConfig('unit.csharp.coverage.match');
|
||
|
$this->excludedFiles = $working->getConfig('unit.csharp.coverage.excluded');
|
||
|
|
||
|
parent::loadEnvironment('unit.csharp.xunit.binary');
|
||
|
|
||
|
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;
|
||
|
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";
|
||
|
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",
|
||
|
$xunit_temp);
|
||
|
} else {
|
||
|
$xunit_args = csprintf(
|
||
|
"%s %s /xml %s /silent",
|
||
|
$this->testEngine,
|
||
|
$test_assembly.".dll",
|
||
|
$xunit_temp);
|
||
|
}
|
||
|
$assembly_dir = $test_assembly."/bin/Debug/";
|
||
|
$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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
$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,
|
||
|
$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;
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|