diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -267,6 +267,7 @@ 'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php', 'ArcanistXUnitTestResultParser' => 'unit/parser/ArcanistXUnitTestResultParser.php', 'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php', + 'MochaTestEngine' => 'unit/engine/MochaTestEngine.php', 'NoseTestEngine' => 'unit/engine/NoseTestEngine.php', 'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php', 'PhpunitTestEngineTestCase' => 'unit/engine/__tests__/PhpunitTestEngineTestCase.php', @@ -540,6 +541,7 @@ 'ArcanistXMLLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistXUnitTestResultParser' => 'Phobject', 'CSharpToolsTestEngine' => 'XUnitTestEngine', + 'MochaTestEngine' => 'ArcanistUnitTestEngine', 'NoseTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngineTestCase' => 'PhutilTestCase', diff --git a/src/unit/engine/MochaTestEngine.php b/src/unit/engine/MochaTestEngine.php new file mode 100644 --- /dev/null +++ b/src/unit/engine/MochaTestEngine.php @@ -0,0 +1,206 @@ +getRunAllTests()) { + $pattern_tests = $this->getAllTestsPattern(); + } else { + $pattern_tests = $this->getGranularTestsPattern(); + } + /* + * Throw an error if there is no test that can be run + */ + if (!$pattern_tests) { + throw new ArcanistNoEffectException(pht('No tests to run.')); + } + /* + * Retrieve working copy and project root path + */ + $working_copy = $this->getWorkingCopy(); + $this->$projectRoot = $working_copy->getProjectRoot(); + /* + * Create new temp-file paths to receive junit test result + * and lcov coverage result only if coverage is activated. + */ + $junit_tmp = new TempFile(); + $cover_tmp = null; + if ($this->getEnableCoverage() !== false) { + $cover_tmp = Filesystem::resolvePath('./coverage/lcov.info', + $this->projectRoot); + } + + $future = $this->buildTestFuture($pattern_tests, $junit_tmp); + list($err, $stdout, $stderr) = $future->resolve(); + + /* + * Check that the future output desired files + */ + if (!Filesystem::pathExists($junit_tmp)) { + throw new CommandException( + pht('Testing Command failed with error #%s!', $err), + $future->getCommand(), + $err, + $stdout, + $stderr); + } + if ($this->getEnableCoverage() !== false) { + if (!Filesystem::pathExists($cover_tmp)) { + throw new CommandException( + pht('Coverage Command failed with error #%s!', $err), + $future->getCommand(), + $err, + $stdout, + $stderr); + } + } + + return $this->parseTestResults($junit_tmp, $cover_tmp); + } + + /** + * Return pattern for retrieving test files + * If special pattern isn't set in configuration file, it is defaulted + */ + private function getAllTestsPattern() { + $pattern = $this->getConfigurationManager() + ->getConfigFromAnySource('unit.mocha.pattern'); + if (!$pattern) { + $pattern = '/tests/*.spec.js'; + } + return $pattern; + } + + /** + * This test engine supports running all tests. + */ + private function getGranularTestsPattern() { + // TODO : build something really granular. + return $this->getAllTestsPattern(); + } + + + private function buildTestFuture($pattern_tests, $junit_tmp) { + $cmd_line = csprintf( + 'MOCHA_FILE=%s'. + 'mocha test --reporter mocha-junit-reporter -u exports -R %s', + $junit_tmp); + + if ($this->getEnableCoverage() !== false) { + $cmd_line = csprintf( + 'MOCHA_FILE=%s'. + 'ISTANBUL_REPORTERS=cobertura'. + 'istanbul cover _mocha'. + '-- -u exports -R %s', + $junit_tmp); + } + + return new ExecFuture('%C %s', $cmd_line, $pattern_tests); + } + + private function parseTestResults($junit_tmp, $cover_tmp) { + $parser = new ArcanistXUnitTestResultParser(); + $results = $parser->parseTestResults(Filesystem::readFile($junit_tmp)); + if ($this->getEnableCoverage() !== false) { + $coverage = $this->readCoverage($cover_tmp); + foreach ($results as $result) { + $result->setCoverage($coverage); + } + } + return $results; + } + + /** + * TODO: This is copied from @{class:NoseTestEngine}. + * I didn't ever try to understand what is going on. + */ + public function readCoverage($cover_file) { + $coverage_dom = new DOMDocument(); + $coverage_dom->loadXML(Filesystem::readFile($cover_file)); + + $reports = array(); + $classes = $coverage_dom->getElementsByTagName('class'); + + foreach ($classes as $class) { + $path = $class->getAttribute('filename'); + $root = $this->getWorkingCopy()->getProjectRoot(); + + if (!Filesystem::isDescendant($path, $root)) { + continue; + } + + // get total line count in file + $lines_list = phutil_split_lines(Filesystem::readFile($path)); + $line_count = count($lines_list); + + $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[$path] = $coverage; + } + + return $reports; + } +}