diff --git a/src/unit/engine/PytestTestEngine.php b/src/unit/engine/PytestTestEngine.php index ca2da944..eeec221f 100644 --- a/src/unit/engine/PytestTestEngine.php +++ b/src/unit/engine/PytestTestEngine.php @@ -8,16 +8,128 @@ final class PytestTestEngine extends ArcanistBaseUnitTestEngine { public function run() { + $working_copy = $this->getWorkingCopy(); + $this->project_root = $working_copy->getProjectRoot(); + $junit_tmp = new TempFile(); + $cover_tmp = new TempFile(); - $cmd_line = csprintf('py.test --junitxml=%s', - $junit_tmp); - $future = new ExecFuture('%C', $cmd_line); - list($stdout, $stderr) = $future->resolvex(); + $future = $this->buildTestFuture($junit_tmp, $cover_tmp); + $future->resolvex(); - $parser = new ArcanistXUnitTestResultParser(); + $future = new ExecFuture('coverage xml -o %s', $cover_tmp); + $future->setCWD($this->project_root); + $future->resolvex(); - return $parser->parseTestResults(Filesystem::readFile($junit_tmp)); + return $this->parseTestResults($junit_tmp, $cover_tmp); } + public function buildTestFuture($junit_tmp, $cover_tmp) { + $paths = $this->getPaths(); + + $cmd_line = csprintf('py.test --junitxml %s', + $junit_tmp); + + if ($this->getEnableCoverage() !== false) { + $cmd_line = csprintf('coverage run --source %s -m %C', + $this->project_root, $cmd_line); + } + + return new ExecFuture('%C', $cmd_line); + } + + public function parseTestResults($junit_tmp, $cover_tmp) { + $parser = new ArcanistXUnitTestResultParser(); + $results = $parser->parseTestResults( + Filesystem::readFile($junit_tmp)); + + if ($this->getEnableCoverage() !== false) { + $coverage_report = $this->readCoverage($cover_tmp); + foreach ($results as $result) { + $result->setCoverage($coverage_report); + } + } + + return $results; + } + + public function readCoverage($path) { + $coverage_data = Filesystem::readFile($path); + if (empty($coverage_data)) { + return array(); + } + + $coverage_dom = new DOMDocument(); + $coverage_dom->loadXML($coverage_data); + + $paths = $this->getPaths(); + $reports = array(); + $classes = $coverage_dom->getElementsByTagName("class"); + + foreach ($classes as $class) { + // filename is actually python module path with ".py" at the end, + // e.g.: tornado.web.py + $relative_path = explode(".", $class->getAttribute("filename")); + array_pop($relative_path); + $relative_path = implode("/", $relative_path); + + // first we check if the path is a directory (a Python package), if it is + // set relative and absolute paths to have __init__.py at the end. + $absolute_path = Filesystem::resolvePath($relative_path); + if (is_dir($absolute_path)) { + $relative_path .= "/__init__.py"; + $absolute_path .= "/__init__.py"; + } + + // then we check if the path with ".py" at the end is file (a Python + // submodule), if it is - set relative and absolute paths to have + // ".py" at the end. + if (is_file($absolute_path.".py")) { + $relative_path .= ".py"; + $absolute_path .= ".py"; + } + + if (!file_exists($absolute_path)) { + continue; + } + + if (!in_array($relative_path, $paths)) { + continue; + } + + // get total line count in file + $line_count = count(file($absolute_path)); + + $coverage = ""; + $start_line = 1; + $lines = $class->getElementsByTagName("line"); + for ($ii = 0; $ii < $lines->length; $ii++) { + $line = $lines->item($ii); + + $next_line = intval($line->getAttribute("number")); + for ($start_line; $start_line < $next_line; $start_line++) { + $coverage .= "N"; + } + + if (intval($line->getAttribute("hits")) == 0) { + $coverage .= "U"; + } + else if (intval($line->getAttribute("hits")) > 0) { + $coverage .= "C"; + } + + $start_line++; + } + + if ($start_line < $line_count) { + foreach (range($start_line, $line_count) as $line_num) { + $coverage .= "N"; + } + } + + $reports[$relative_path] = $coverage; + } + + return $reports; + } }