4
* Very basic 'nose' unit test engine wrapper.
6
* Requires nose 1.1.3 for code coverage.
8
final class NoseTestEngine extends ArcanistUnitTestEngine {
10
public function run() {
11
$paths = $this->getPaths();
13
$affected_tests = array();
14
foreach ($paths as $path) {
15
$absolute_path = Filesystem::resolvePath($path);
17
if (is_dir($absolute_path)) {
18
$absolute_test_path = Filesystem::resolvePath('tests/'.$path);
19
if (is_readable($absolute_test_path)) {
20
$affected_tests[] = $absolute_test_path;
24
if (is_readable($absolute_path)) {
25
$filename = basename($path);
26
$directory = dirname($path);
28
// assumes directory layout: tests/<package>/test_<module>.py
29
$relative_test_path = 'tests/'.$directory.'/test_'.$filename;
30
$absolute_test_path = Filesystem::resolvePath($relative_test_path);
32
if (is_readable($absolute_test_path)) {
33
$affected_tests[] = $absolute_test_path;
38
return $this->runTests($affected_tests, './');
41
public function runTests($test_paths, $source_path) {
42
if (empty($test_paths)) {
48
foreach ($test_paths as $test_path) {
49
$xunit_tmp = new TempFile();
50
$cover_tmp = new TempFile();
52
$future = $this->buildTestFuture($test_path, $xunit_tmp, $cover_tmp);
54
$futures[$test_path] = $future;
55
$tmpfiles[$test_path] = array(
56
'xunit' => $xunit_tmp,
57
'cover' => $cover_tmp,
62
foreach (Futures($futures)->limit(4) as $test_path => $future) {
64
list($stdout, $stderr) = $future->resolvex();
65
} catch(CommandException $exc) {
66
if ($exc->getError() > 1) {
67
// 'nose' returns 1 when tests are failing/broken.
72
$xunit_tmp = $tmpfiles[$test_path]['xunit'];
73
$cover_tmp = $tmpfiles[$test_path]['cover'];
75
$this->parser = new ArcanistXUnitTestResultParser();
76
$results[] = $this->parseTestResults($source_path,
81
return array_mergev($results);
84
public function buildTestFuture($path, $xunit_tmp, $cover_tmp) {
85
$cmd_line = csprintf('nosetests --with-xunit --xunit-file=%s',
88
if ($this->getEnableCoverage() !== false) {
89
$cmd_line .= csprintf(
90
' --with-coverage --cover-xml --cover-xml-file=%s',
94
return new ExecFuture('%C %s', $cmd_line, $path);
97
public function parseTestResults($source_path, $xunit_tmp, $cover_tmp) {
98
$results = $this->parser->parseTestResults(
99
Filesystem::readFile($xunit_tmp));
101
// coverage is for all testcases in the executed $path
102
if ($this->getEnableCoverage() !== false) {
103
$coverage = $this->readCoverage($cover_tmp, $source_path);
104
foreach ($results as $result) {
105
$result->setCoverage($coverage);
112
public function readCoverage($cover_file, $source_path) {
113
$coverage_dom = new DOMDocument();
114
$coverage_dom->loadXML(Filesystem::readFile($cover_file));
117
$classes = $coverage_dom->getElementsByTagName('class');
119
foreach ($classes as $class) {
120
$path = $class->getAttribute('filename');
121
$root = $this->getWorkingCopy()->getProjectRoot();
123
if (!Filesystem::isDescendant($path, $root)) {
127
// get total line count in file
128
$line_count = count(phutil_split_lines(Filesystem::readFile($path)));
132
$lines = $class->getElementsByTagName('line');
133
for ($ii = 0; $ii < $lines->length; $ii++) {
134
$line = $lines->item($ii);
136
$next_line = intval($line->getAttribute('number'));
137
for ($start_line; $start_line < $next_line; $start_line++) {
141
if (intval($line->getAttribute('hits')) == 0) {
143
} else if (intval($line->getAttribute('hits')) > 0) {
150
if ($start_line < $line_count) {
151
foreach (range($start_line, $line_count) as $line_num) {
156
$reports[$path] = $coverage;