Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14319620
D13949.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
6 KB
Referenced Files
None
Subscribers
None
D13949.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D13949: Add Mocha TestEngine (not worth review / ready for upstream yet)
Attached
Detach File
Event Timeline
Log In to Comment