Page MenuHomePhabricator

D13949.diff
No OneTemporary

D13949.diff

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
@@ -279,6 +279,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',
@@ -564,6 +565,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,163 @@
+<?php
+
+/**
+ * Uses [[https://mochajs.org/ | Mocha ]] to test Javascript code.
+ * Uses [[ https://gotwarlost.github.io/istanbul | Istambul ]]
+ * to cover Javascript code.
+ *
+ * Assumes that when modifying a file with a path like `SomeDir/SomeFile.js`,
+ * that the unit test that verifies the functionality of `SomeFile` is
+ * located by default at `SomeDir/__tests__/SomeFile-test.js`.
+ */
+final class MochaTestEngine extends ArcanistUnitTestEngine {
+ public function getEngineConfigurationName() {
+ return 'mocha';
+ }
+
+ protected function supportsRunAllTests() {
+ return true;
+ }
+
+ public function run() {
+ $pattern_tests = $this->getAllTestsPattern();
+
+ if (!$pattern_tests) {
+ throw new ArcanistNoEffectException(pht('No tests to run.'));
+ }
+
+ $working_copy = $this->getWorkingCopy();
+ $project_root = $working_copy->getProjectRoot();
+
+ $junit_tmp = new TempFile();
+ $cover_tmp = null;
+
+ if ($this->getEnableCoverage()) {
+ $cover_path = './coverage/cobertura-coverage.xml';
+ $cover_tmp = Filesystem::resolvePath($cover_path, $project_root);
+ }
+
+ $future = $this->buildTestFuture($pattern_tests, $junit_tmp);
+ list($err, $stdout, $stderr) = $future->resolve();
+
+ if (!Filesystem::pathExists($junit_tmp)) {
+ throw new CommandException(
+ pht('Testing Command failed with error #%s!', $err),
+ $future->getCommand(),
+ $err,
+ $stdout,
+ $stderr);
+ }
+ if ($this->getEnableCoverage()) {
+ 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);
+ }
+
+ private function getAllTestsPattern() {
+ $pattern = $this->getConfigurationManager()
+ ->getConfigFromAnySource('unit.mocha.pattern');
+ if (!$pattern) {
+ $pattern = 'tests/*.spec.js';
+ }
+ return $pattern;
+ }
+
+
+ private function buildTestFuture($pattern_tests, $junit_tmp) {
+ $cmd_line = csprintf(
+ 'MOCHA_FILE=%s '.
+ 'mocha test -u exports '.
+ '--reporter mocha-junit-reporter %s',
+ $junit_tmp, $pattern_tests);
+
+ if ($this->getEnableCoverage()) {
+ $cmd_line = csprintf(
+ 'MOCHA_FILE=%s '.
+ 'istanbul cover --report cobertura '.
+ 'node_modules/mocha/bin/_mocha '.
+ '-- -u exports '.
+ '--compilers js:babel/register-without-polyfill '.
+ // Maybe make previous line optional.
+ // It is useful to run test when code use ES6
+ '--reporter mocha-junit-reporter %s',
+ $junit_tmp, $pattern_tests);
+ }
+
+ return new ExecFuture($cmd_line);
+ }
+
+ private function parseTestResults($junit_tmp, $cover_tmp) {
+ $parser = new ArcanistXUnitTestResultParser();
+ $results = $parser->parseTestResults(Filesystem::readFile($junit_tmp));
+ if ($this->getEnableCoverage()) {
+ $coverage = $this->readCoverage($cover_tmp);
+ foreach ($results as $result) {
+ $result->setCoverage($coverage);
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * 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;
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Dec 19, 10:00 AM (5 h, 25 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6906981
Default Alt Text
D13949.diff (6 KB)

Event Timeline