1<?php 2 3/** 4 * Very basic 'nose' unit test engine wrapper. 5 * 6 * Requires nose 1.1.3 for code coverage. 7 */ 8final class NoseTestEngine extends ArcanistUnitTestEngine { 9 10 private $parser; 11 12 protected function supportsRunAllTests() { 13 return true; 14 } 15 16 public function run() { 17 if ($this->getRunAllTests()) { 18 $root = $this->getWorkingCopy()->getProjectRoot(); 19 $all_tests = glob(Filesystem::resolvePath("$root/tests/**/test_*.py")); 20 return $this->runTests($all_tests, $root); 21 } 22 23 $paths = $this->getPaths(); 24 25 $affected_tests = array(); 26 foreach ($paths as $path) { 27 $absolute_path = Filesystem::resolvePath($path); 28 29 if (is_dir($absolute_path)) { 30 $absolute_test_path = Filesystem::resolvePath('tests/'.$path); 31 if (is_readable($absolute_test_path)) { 32 $affected_tests[] = $absolute_test_path; 33 } 34 } 35 36 if (is_readable($absolute_path)) { 37 $filename = basename($path); 38 $directory = dirname($path); 39 40 // assumes directory layout: tests/<package>/test_<module>.py 41 $relative_test_path = 'tests/'.$directory.'/test_'.$filename; 42 $absolute_test_path = Filesystem::resolvePath($relative_test_path); 43 44 if (is_readable($absolute_test_path)) { 45 $affected_tests[] = $absolute_test_path; 46 } 47 } 48 } 49 50 return $this->runTests($affected_tests, './'); 51 } 52 53 public function runTests($test_paths, $source_path) { 54 if (empty($test_paths)) { 55 return array(); 56 } 57 58 $futures = array(); 59 $tmpfiles = array(); 60 foreach ($test_paths as $test_path) { 61 $xunit_tmp = new TempFile(); 62 $cover_tmp = new TempFile(); 63 64 $future = $this->buildTestFuture($test_path, $xunit_tmp, $cover_tmp); 65 66 $futures[$test_path] = $future; 67 $tmpfiles[$test_path] = array( 68 'xunit' => $xunit_tmp, 69 'cover' => $cover_tmp, 70 ); 71 } 72 73 $results = array(); 74 $futures = id(new FutureIterator($futures)) 75 ->limit(4); 76 foreach ($futures as $test_path => $future) { 77 try { 78 list($stdout, $stderr) = $future->resolvex(); 79 } catch (CommandException $exc) { 80 if ($exc->getError() > 1) { 81 // 'nose' returns 1 when tests are failing/broken. 82 throw $exc; 83 } 84 } 85 86 $xunit_tmp = $tmpfiles[$test_path]['xunit']; 87 $cover_tmp = $tmpfiles[$test_path]['cover']; 88 89 $this->parser = new ArcanistXUnitTestResultParser(); 90 $results[] = $this->parseTestResults( 91 $source_path, 92 $xunit_tmp, 93 $cover_tmp); 94 } 95 96 return array_mergev($results); 97 } 98 99 public function buildTestFuture($path, $xunit_tmp, $cover_tmp) { 100 $cmd_line = csprintf( 101 'nosetests --with-xunit --xunit-file=%s', 102 $xunit_tmp); 103 104 if ($this->getEnableCoverage() !== false) { 105 $cmd_line .= csprintf( 106 ' --with-coverage --cover-xml --cover-xml-file=%s', 107 $cover_tmp); 108 } 109 110 return new ExecFuture('%C %s', $cmd_line, $path); 111 } 112 113 public function parseTestResults($source_path, $xunit_tmp, $cover_tmp) { 114 $results = $this->parser->parseTestResults( 115 Filesystem::readFile($xunit_tmp)); 116 117 // coverage is for all testcases in the executed $path 118 if ($this->getEnableCoverage() !== false) { 119 $coverage = $this->readCoverage($cover_tmp, $source_path); 120 foreach ($results as $result) { 121 $result->setCoverage($coverage); 122 } 123 } 124 125 return $results; 126 } 127 128 public function readCoverage($cover_file, $source_path) { 129 $coverage_xml = Filesystem::readFile($cover_file); 130 if (strlen($coverage_xml) < 1) { 131 return array(); 132 } 133 $coverage_dom = new DOMDocument(); 134 $coverage_dom->loadXML($coverage_xml); 135 136 $reports = array(); 137 $classes = $coverage_dom->getElementsByTagName('class'); 138 139 foreach ($classes as $class) { 140 $path = $class->getAttribute('filename'); 141 $root = $this->getWorkingCopy()->getProjectRoot(); 142 143 if (!Filesystem::isDescendant($path, $root)) { 144 continue; 145 } 146 147 // get total line count in file 148 $line_count = count(phutil_split_lines(Filesystem::readFile($path))); 149 150 $coverage = ''; 151 $start_line = 1; 152 $lines = $class->getElementsByTagName('line'); 153 for ($ii = 0; $ii < $lines->length; $ii++) { 154 $line = $lines->item($ii); 155 156 $next_line = (int)$line->getAttribute('number'); 157 for ($start_line; $start_line < $next_line; $start_line++) { 158 $coverage .= 'N'; 159 } 160 161 if ((int)$line->getAttribute('hits') == 0) { 162 $coverage .= 'U'; 163 } else if ((int)$line->getAttribute('hits') > 0) { 164 $coverage .= 'C'; 165 } 166 167 $start_line++; 168 } 169 170 if ($start_line < $line_count) { 171 foreach (range($start_line, $line_count) as $line_num) { 172 $coverage .= 'N'; 173 } 174 } 175 176 $reports[$path] = $coverage; 177 } 178 179 return $reports; 180 } 181 182} 183