Bug 1498183 - Supporting async/await for mochitest and xpcshell test in indexedDB; r=asuth
authorTom Tung <shes050117@gmail.com>
Thu, 17 Jan 2019 15:05:21 +0000
changeset 511384 4a74ab74ff7a41cafaaa2829e166652680135c2f
parent 511383 821f274155c761e7de874f1b0ca86d6f1492fe47
child 511385 67fa6cd74c6a138415b7a15959057a6e82c20628
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth
bugs1498183
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1498183 - Supporting async/await for mochitest and xpcshell test in indexedDB; r=asuth This patch mainly uses add_task while running async/await scripts. To support that in workers, it also changes testHarness logic for worker from "yield" to promise. Differential Revision: https://phabricator.services.mozilla.com/D15714
dom/indexedDB/test/helpers.js
dom/indexedDB/test/unit/test_constraint_error_messages.js
dom/indexedDB/test/unit/xpcshell-head-parent-process.js
--- a/dom/indexedDB/test/helpers.js
+++ b/dom/indexedDB/test/helpers.js
@@ -1,17 +1,20 @@
 /**
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // testSteps is expected to be defined by the test using this file.
 /* global testSteps:false */
 
-var testGenerator = testSteps();
+var testGenerator;
+if (testSteps.constructor.name === "GeneratorFunction") {
+  testGenerator = testSteps();
+}
 // The test js is shared between xpcshell (which has no SpecialPowers object)
 // and content mochitests (where the |Components| object is accessible only as
 // SpecialPowers.Components). Expose Components if necessary here to make things
 // work everywhere.
 //
 // Even if the real |Components| doesn't exist, we might shim in a simple JS
 // placebo for compat. An easy way to differentiate this from the real thing
 // is whether the property is read-only or not.
@@ -41,16 +44,22 @@ function clearAllDatabases(callback) {
 var testHarnessGenerator = testHarnessSteps();
 testHarnessGenerator.next();
 
 function* testHarnessSteps() {
   function nextTestHarnessStep(val) {
     testHarnessGenerator.next(val);
   }
 
+  let script = document.createElement("script");
+  script.src = "/tests/SimpleTest/AddTask.js";
+  script.onload = nextTestHarnessStep;
+  document.head.appendChild(script);
+  yield undefined;
+
   let testScriptPath;
   let testScriptFilename;
 
   let scripts = document.getElementsByTagName("script");
   for (let i = 0; i < scripts.length; i++) {
     let src = scripts[i].src;
     let match = src.match(/indexedDB\/test\/unit\/(test_[^\/]+\.js)$/);
     if (match && match.length == 2) {
@@ -94,129 +103,78 @@ function* testHarnessSteps() {
   yield undefined;
 
   info("Clearing old databases");
 
   clearAllDatabases(nextTestHarnessStep);
   yield undefined;
 
   if (testScriptFilename && !window.disableWorkerTest) {
-    info("Running test in a worker");
-
-    let workerScriptBlob =
-      new Blob([ "(" + workerScript.toString() + ")();" ],
-               { type: "text/javascript" });
-    let workerScriptURL = URL.createObjectURL(workerScriptBlob);
-
-    let worker = new Worker(workerScriptURL);
-
-    worker._expectingUncaughtException = false;
-    worker.onerror = function(event) {
-      if (worker._expectingUncaughtException) {
-        ok(true, "Worker had an expected error: " + event.message);
-        worker._expectingUncaughtException = false;
-        event.preventDefault();
-        return;
-      }
-      ok(false, "Worker had an error: " + event.message);
-      worker.terminate();
-      nextTestHarnessStep();
-    };
-
-    worker.onmessage = function(event) {
-      let message = event.data;
-      switch (message.op) {
-        case "ok":
-          SimpleTest.ok(message.condition, `${message.name}: ${message.diag}`);
-          break;
-
-        case "todo":
-          todo(message.condition, message.name, message.diag);
-          break;
-
-        case "info":
-          info(message.msg);
-          break;
-
-        case "ready":
-          worker.postMessage({ op: "load", files: [ testScriptPath ] });
-          break;
+    // For the AsyncFunction, let AddTask.js handle the executing sequece by
+    // add_task(). For the GeneratorFunction, we just handle the sequence
+    // manually.
+    if (testSteps.constructor.name === "AsyncFunction") {
+      add_task(function workerTestSteps() {
+        return executeWorkerTestAndCleanUp(testScriptPath);
+      });
+    } else {
+      ok(testSteps.constructor.name === "GeneratorFunction",
+         "Unsupported function type");
+      executeWorkerTestAndCleanUp(testScriptPath)
+        .then(nextTestHarnessStep);
 
-        case "loaded":
-          worker.postMessage({ op: "start", wasmSupported: isWasmSupported() });
-          break;
-
-        case "done":
-          ok(true, "Worker finished");
-          nextTestHarnessStep();
-          break;
-
-        case "expectUncaughtException":
-          worker._expectingUncaughtException = message.expecting;
-          break;
-
-        case "clearAllDatabases":
-          clearAllDatabases(function() {
-            worker.postMessage({ op: "clearAllDatabasesDone" });
-          });
-          break;
-
-        case "getWasmBinary":
-          worker.postMessage({ op: "getWasmBinaryDone",
-                               wasmBinary: getWasmBinarySync(message.text) });
-          break;
-
-        default:
-          ok(false,
-             "Received a bad message from worker: " + JSON.stringify(message));
-          nextTestHarnessStep();
-      }
-    };
-
-    URL.revokeObjectURL(workerScriptURL);
-
-    yield undefined;
-
-    if (worker._expectingUncaughtException) {
-      ok(false, "expectUncaughtException was called but no uncaught " +
-                "exception was detected!");
+      yield undefined;
     }
-
-    worker.terminate();
-    worker = null;
-
-    clearAllDatabases(nextTestHarnessStep);
-    yield undefined;
   } else if (testScriptFilename) {
     todo(false,
          "Skipping test in a worker because it is explicitly disabled: " +
          window.disableWorkerTest);
   } else {
     todo(false,
          "Skipping test in a worker because it's not structured properly");
   }
 
   info("Running test in main thread");
 
   // Now run the test script in the main thread.
-  testGenerator.next();
+  if (testSteps.constructor.name === "AsyncFunction") {
+    // Register a callback to clean up databases because it's the only way for
+    // add_task() to clean them right before the SimpleTest.FinishTest
+    SimpleTest.registerCleanupFunction(async function() {
+      await new Promise(function(resolve, reject) {
+        clearAllDatabases(function(result) {
+          if (result.resultCode == SpecialPowers.Cr.NS_OK) {
+            resolve(result);
+          } else {
+            reject(result.resultCode);
+          }
+        });
+      });
+    });
 
-  yield undefined;
+    add_task(testSteps);
+  } else {
+    testGenerator.next();
+
+    yield undefined;
+  }
 }
 
 if (!window.runTest) {
   window.runTest = function()
   {
     SimpleTest.waitForExplicitFinish();
     testHarnessGenerator.next();
   };
 }
 
 function finishTest()
 {
+  ok(testSteps.constructor.name === "GeneratorFunction",
+     "Async/await tests shouldn't call finishTest()");
   SimpleTest.executeSoon(function() {
     clearAllDatabases(function() { SimpleTest.finish(); });
   });
 }
 
 function browserRunTest()
 {
   testGenerator.next();
@@ -371,16 +329,48 @@ function getWasmBinary(text) {
     testGenerator.next(binary);
   });
 }
 function getWasmModule(_binary_) {
   let module = new WebAssembly.Module(_binary_);
   return module;
 }
 
+function expectingSuccess(request) {
+  return new Promise(function(resolve, reject) {
+    request.onerror = function(event) {
+      ok(false, "indexedDB error, '" + event.target.error.name + "'");
+      reject(event);
+    };
+    request.onsuccess = function(event) {
+      resolve(event);
+    };
+    request.onupgradeneeded = function(event) {
+      ok(false, "Got upgrade, but did not expect it!");
+      reject(event);
+    };
+  });
+}
+
+function expectingUpgrade(request) {
+  return new Promise(function(resolve, reject) {
+    request.onerror = function(event) {
+      ok(false, "indexedDB error, '" + event.target.error.name + "'");
+      reject(event);
+    };
+    request.onupgradeneeded = function(event) {
+      resolve(event);
+    };
+    request.onsuccess = function(event) {
+      ok(false, "Got success, but did not expect it!");
+      reject(event);
+    };
+  });
+}
+
 function workerScript() {
   "use strict";
 
   self.wasmSupported = false;
 
   self.repr = function(_thing_) {
     if (typeof(_thing_) == "undefined") {
       return "undefined";
@@ -437,19 +427,21 @@ function workerScript() {
 
   self.executeSoon = function(_fun_) {
     var channel = new MessageChannel();
     channel.port1.postMessage("");
     channel.port2.onmessage = function(event) { _fun_(); };
   };
 
   self.finishTest = function() {
+    self.ok(testSteps.constructor.name === "GeneratorFunction",
+            "Async/await tests shouldn't call finishTest()");
     if (self._expectingUncaughtException) {
-      self.ok(false, "expectUncaughtException was called but no uncaught "
-                     + "exception was detected!");
+      self.ok(false, "expectUncaughtException was called but no uncaught " +
+                     "exception was detected!");
     }
     self.postMessage({ op: "done" });
   };
 
   self.grabEventAndContinueHandler = function(_event_) {
     testGenerator.next(_event_);
   };
 
@@ -596,19 +588,30 @@ function workerScript() {
       case "load":
         info("Worker: loading " + JSON.stringify(message.files));
         self.importScripts(message.files);
         self.postMessage({ op: "loaded" });
         break;
 
       case "start":
         self.wasmSupported = message.wasmSupported;
-        executeSoon(function() {
+        executeSoon(async function() {
           info("Worker: starting tests");
-          testGenerator.next();
+          if (testSteps.constructor.name === "AsyncFunction") {
+            await testSteps();
+            if (self._expectingUncaughtException) {
+              self.ok(false, "expectUncaughtException was called but no " +
+                             "uncaught exception was detected!");
+            }
+            self.postMessage({ op: "done" });
+          } else {
+            ok(testSteps.constructor.name === "GeneratorFunction",
+               "Unsupported function type");
+            testGenerator.next();
+          }
         });
         break;
 
       case "clearAllDatabasesDone":
         info("Worker: all databases are cleared");
         if (self._clearAllDatabasesCallback) {
           self._clearAllDatabasesCallback();
         }
@@ -620,10 +623,140 @@ function workerScript() {
         break;
 
       default:
         throw new Error("Received a bad message from parent: " +
                         JSON.stringify(message));
     }
   };
 
+
+  self.expectingSuccess = function(_request_) {
+    return new Promise(function(_resolve_, _reject_) {
+      _request_.onerror = function(_event_) {
+        ok(false, "indexedDB error, '" + _event_.target.error.name + "'");
+        _reject_(_event_);
+      };
+      _request_.onsuccess = function(_event_) {
+        _resolve_(_event_);
+      };
+      _request_.onupgradeneeded = function(_event_) {
+        ok(false, "Got upgrade, but did not expect it!");
+        _reject_(_event_);
+        };
+      });
+  };
+
+  self.expectingUpgrade = function(_request_) {
+    return new Promise(function(_resolve_, _reject_) {
+      _request_.onerror = function(_event_) {
+        ok(false, "indexedDB error, '" + _event_.target.error.name + "'");
+        _reject_(_event_);
+      };
+      _request_.onupgradeneeded = function(_event_) {
+        _resolve_(_event_);
+      };
+      _request_.onsuccess = function(_event_) {
+        ok(false, "Got success, but did not expect it!");
+        _reject_(_event_);
+      };
+    });
+ };
+
   self.postMessage({ op: "ready" });
 }
+
+async function executeWorkerTestAndCleanUp(testScriptPath) {
+  info("Running test in a worker");
+
+  let workerScriptBlob =
+    new Blob([ "(" + workerScript.toString() + ")();" ],
+             { type: "text/javascript" });
+  let workerScriptURL = URL.createObjectURL(workerScriptBlob);
+
+  let worker;
+  try {
+    await new Promise(function(resolve, reject) {
+      worker = new Worker(workerScriptURL);
+
+      worker._expectingUncaughtException = false;
+      worker.onerror = function(event) {
+        if (worker._expectingUncaughtException) {
+          ok(true, "Worker had an expected error: " + event.message);
+          worker._expectingUncaughtException = false;
+          event.preventDefault();
+          return;
+        }
+        ok(false, "Worker had an error: " + event.message);
+        worker.terminate();
+        reject();
+      };
+
+      worker.onmessage = function(event) {
+        let message = event.data;
+        switch (message.op) {
+          case "ok":
+            SimpleTest.ok(message.condition, `${message.name}: ${message.diag}`);
+            break;
+
+          case "todo":
+            todo(message.condition, message.name, message.diag);
+            break;
+
+          case "info":
+            info(message.msg);
+            break;
+
+          case "ready":
+            worker.postMessage({ op: "load", files: [ testScriptPath ] });
+            break;
+
+          case "loaded":
+            worker.postMessage({ op: "start", wasmSupported: isWasmSupported() });
+            break;
+
+          case "done":
+            ok(true, "Worker finished");
+            resolve();
+            break;
+
+          case "expectUncaughtException":
+            worker._expectingUncaughtException = message.expecting;
+            break;
+
+          case "clearAllDatabases":
+            clearAllDatabases(function() {
+              worker.postMessage({ op: "clearAllDatabasesDone" });
+            });
+            break;
+
+          case "getWasmBinary":
+             worker.postMessage({ op: "getWasmBinaryDone",
+                                 wasmBinary: getWasmBinarySync(message.text) });
+             break;
+
+          default:
+            ok(false,
+               "Received a bad message from worker: " + JSON.stringify(message));
+            reject();
+        }
+      };
+    });
+
+    URL.revokeObjectURL(workerScriptURL);
+  } catch (e) {
+    info("Unexpected thing happened: " + e);
+  }
+
+  return new Promise(function(resolve) {
+    info("Cleaning up the databases");
+
+    if (worker._expectingUncaughtException) {
+      ok(false, "expectUncaughtException was called but no uncaught " +
+                "exception was detected!");
+    }
+
+    worker.terminate();
+    worker = null;
+
+    clearAllDatabases(resolve);
+  });
+}
--- a/dom/indexedDB/test/unit/test_constraint_error_messages.js
+++ b/dom/indexedDB/test/unit/test_constraint_error_messages.js
@@ -1,29 +1,24 @@
 /**
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
-var testGenerator = testSteps();
-
-function* testSteps()
+async function testSteps()
 {
   const name = this.window ? window.location.pathname : "Splendid Test";
   const objectStoreName = "foo";
   const indexName = "bar", keyPath = "bar";
 
   info("Opening database");
 
   let request = indexedDB.open(name);
-  request.onerror = errorHandler;
-  request.onupgradeneeded = grabEventAndContinueHandler;
-  request.onsuccess = unexpectedSuccessHandler;
+  let event =  await expectingUpgrade(request);
 
-  let event = yield undefined;
   let db = event.target.result;
 
   info("Creating objectStore");
 
   let objectStore = db.createObjectStore(objectStoreName);
 
   info("Creating a duplicated object store to get an error");
 
@@ -51,14 +46,11 @@ function* testSteps()
     ok(false, "ConstraintError should be thrown if index already exists");
   } catch (e) {
     ok(true, "ConstraintError should be thrown if index already exists");
     is(e.message,
        "Index named '" + indexName + "' already exists at index '0'",
        "Threw with correct error message");
   }
 
-  request.onsuccess = grabEventAndContinueHandler;
-  yield undefined;
+  await expectingSuccess(request);
   db.close();
-
-  finishTest();
 }
--- a/dom/indexedDB/test/unit/xpcshell-head-parent-process.js
+++ b/dom/indexedDB/test/unit/xpcshell-head-parent-process.js
@@ -1,15 +1,17 @@
 /**
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests using testGenerator are expected to define it themselves.
-/* global testGenerator */
+// Testing functions are expected to call testSteps and its type should either
+// be GeneratorFunction or AsyncFunction
+/* global testGenerator, testSteps:false */
 
 var { "classes": Cc, "interfaces": Ci, "utils": Cu } = Components;
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 if (!("self" in this)) {
   this.self = this;
 }
@@ -44,18 +46,38 @@ if (!this.runTest) {
       do_get_profile();
 
       enableTesting();
       enableExperimental();
     }
 
     Cu.importGlobalProperties(["indexedDB"]);
 
-    do_test_pending();
-    testGenerator.next();
+    // In order to support converting tests to using async functions from using
+    // generator functions, we detect async functions by checking the name of
+    // function's constructor.
+    Assert.ok(typeof testSteps === "function",
+              "There should be a testSteps function");
+    if (testSteps.constructor.name === "AsyncFunction") {
+      // Do run our existing cleanup function that would normally be called by
+      // the generator's call to finishTest().
+      registerCleanupFunction(resetTesting);
+
+      add_task(testSteps);
+
+      // Since we defined run_test, we must invoke run_next_test() to start the
+      // async test.
+      run_next_test();
+    } else {
+      Assert.ok(testSteps.constructor.name === "GeneratorFunction",
+                "Unsupported function type");
+
+      do_test_pending();
+      testGenerator.next();
+    }
   };
 }
 
 function finishTest()
 {
   if (SpecialPowers.isMainProcess()) {
     resetExperimental();
     resetTesting();
@@ -529,16 +551,48 @@ function setMaxSerializedMsgSize(aSize)
 }
 
 function getPrincipal(url)
 {
   let uri = Services.io.newURI(url);
   return Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
 }
 
+function expectingSuccess(request) {
+  return new Promise(function(resolve, reject) {
+    request.onerror = function(event) {
+      ok(false, "indexedDB error, '" + event.target.error.name + "'");
+      reject(event);
+    };
+    request.onsuccess = function(event) {
+      resolve(event);
+    };
+    request.onupgradeneeded = function(event) {
+      ok(false, "Got upgrade, but did not expect it!");
+      reject(event);
+    };
+  });
+}
+
+function expectingUpgrade(request) {
+  return new Promise(function(resolve, reject) {
+    request.onerror = function(event) {
+      ok(false, "indexedDB error, '" + event.target.error.name + "'");
+      reject(event);
+    };
+    request.onupgradeneeded = function(event) {
+      resolve(event);
+    };
+    request.onsuccess = function(event) {
+      ok(false, "Got success, but did not expect it!");
+      reject(event);
+    };
+  });
+}
+
 var SpecialPowers = {
   isMainProcess() {
     return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
   },
   notifyObservers(subject, topic, data) {
     Services.obs.notifyObservers(subject, topic, data);
   },
   notifyObserversInParentProcess(subject, topic, data) {