1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-14 19:02:40 +01:00
phorge-arcanist/src/unit/engine/CSharpToolsTestEngine.php
Joshua Spence d2b38cdf94 pht all the things
Summary: `pht`ize almost all strings in rARC.

Test Plan: ¯\_(ツ)_/¯

Reviewers: #blessed_reviewers, epriestley

Reviewed By: #blessed_reviewers, epriestley

Subscribers: aurelijus, Korvin, epriestley

Differential Revision: https://secure.phabricator.com/D12607
2015-05-13 21:00:53 +10:00

287 lines
8.6 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.
*/
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;
}
}