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($config_item); 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; } }