rest of the test logic draft divert-ipc-fix
authorAndrew Sutherland <asutherland@asutherland.org>
Thu, 04 Jan 2018 18:38:43 -0500
changeset 435931 7860400a0288e0ae009aedfce6899a44b4f0c8a4
parent 435930 34f44c6d88523246e57a17d023c5d53ce7402a68
push id7
push userbugmail@asutherland.org
push dateThu, 04 Jan 2018 23:45:14 +0000
milestone59.0a1
rest of the test logic
dom/workers/test/serviceworkers/browser_download_canceled.js
dom/workers/test/serviceworkers/download_canceled/page_download_canceled.html
dom/workers/test/serviceworkers/download_canceled/server-stream-download.sjs
dom/workers/test/serviceworkers/download_canceled/sw_download_canceled.js
--- a/dom/workers/test/serviceworkers/browser_download_canceled.js
+++ b/dom/workers/test/serviceworkers/browser_download_canceled.js
@@ -80,27 +80,45 @@ async function performCanceledDownload(t
   const cancelDownload = promiseClickDownloadDialogButton("cancel");
 
   // Trigger the download.
   info(`triggering download of "${path}"`);
   await ContentTask.spawn(
     tab.linkedBrowser,
     path,
     function(path) {
+      // Put a Promise in place that we can wait on for stream closure.
+      content.wrappedJSObject.trackStreamClosure(path);
+      // Create the link and trigger the download.
       const link = content.document.createElement('a');
       link.href = path;
       link.download = path;
       content.document.body.appendChild(link);
       link.click();
     });
 
   // Wait for the cancelation to have been triggered.
   info("waiting for download popup");
   await cancelDownload;
-  ok("canceled download");
+  ok(true, "canceled download");
+
+  // Wait for confirmation that the stream stopped.
+  info(`wait for the ${path} stream to close.`);
+  const why = await ContentTask.spawn(
+    tab.linkedBrowser,
+    path,
+    function(path) {
+      return content.wrappedJSObject.streamClosed[path].promise;
+    });
+  is(why.why, "canceled", "Ensure the stream canceled instead of timing out.");
+  // Note that for the "sw-stream-download" case, we end up with a bogus
+  // reason of "'close' may only be called on a stream in the 'readable' state."
+  // Since we aren't actually invoking close(), I'm assuming this is an
+  // implementation bug that will be corrected in the web platform tests.
+  info(`Cancellation reason: ${why.message} after ${why.ticks} ticks`);
 }
 
 const gTestRoot = getRootDirectory(gTestPath)
   .replace("chrome://mochitests/content/", "http://mochi.test:8888/");
 
 
 const PAGE_URL = `${gTestRoot}download_canceled/page_download_canceled.html`;
 
--- a/dom/workers/test/serviceworkers/download_canceled/page_download_canceled.html
+++ b/dom/workers/test/serviceworkers/download_canceled/page_download_canceled.html
@@ -23,12 +23,31 @@ function wait_until_controlled() {
     });
   });
 }
 addEventListener('load', async function(event) {
   window.controlled = wait_until_controlled();
   window.registration =
     await navigator.serviceWorker.register('sw_download_canceled.js');
 });
+
+// Place to hold promises for stream closures reported by the SW.
+window.streamClosed = {};
+
+// The ServiceWorker will postMessage to this BroadcastChannel when the streams
+// are closed.  (Alternately, the SW could have used the clients API to post at
+// us, but the mechanism by which that operates would be different when this
+// test is uplifted, and it's desirable to avoid timing changes.)
+//
+// The browser test will use this promise to wait on stream shutdown.
+window.swStreamChannel = new BroadcastChannel("stream-closed");
+function trackStreamClosure(path) {
+  let resolve;
+  const promise = new Promise(r => { resolve = r });
+  window.streamClosed[path] = { promise, resolve };
+}
+window.swStreamChannel.onmessage = ({ data }) => {
+  window.streamClosed[data.what].resolve(data);
+};
 </script>
 
 </body>
 </html>
--- a/dom/workers/test/serviceworkers/download_canceled/server-stream-download.sjs
+++ b/dom/workers/test/serviceworkers/download_canceled/server-stream-download.sjs
@@ -1,45 +1,121 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
-/**
- * Provide search suggestions in the OpenSearch JSON format.
+// stolen from file_blocked_script.sjs
+function setGlobalState(data, key)
+{
+  x = { data: data, QueryInterface: function(iid) { return this } };
+  x.wrappedJSObject = x;
+  setObjectState(key, x);
+}
+
+function getGlobalState(key)
+{
+  var data;
+  getObjectState(key, function(x) {
+    data = x && x.wrappedJSObject.data;
+  });
+  return data;
+}
+
+/*
+ * We want to let the sw_download_canceled.js service worker know when the
+ * stream was canceled.  To this end, we let it issue a monitor request which we
+ * fulfill when the stream has been canceled.  In order to coordinate between
+ * multiple requests, we use the getObjectState/setObjectState mechanism that
+ * httpd.js exposes to let data be shared and/or persist between requests.  We
+ * handle both possible orderings of the requests because we currently don't
+ * try and impose an ordering between the two requests as issued by the SW, and
+ * file_blocked_script.sjs encourages us to do this, but we probably could order
+ * them.
  */
-const MAX_COUNT = 3000;
-function handleRequest(request, response) {
+const MONITOR_KEY = "stream-monitor";
+function completeMonitorResponse(response, data) {
+  response.write(JSON.stringify(data));
+  response.finish();
+}
+function handleMonitorRequest(request, response) {
+  response.setHeader("Content-Type", "application/json");
+  response.setStatusLine(request.httpVersion, 200, "Found");
+
+  response.processAsync();
+  // Necessary to cause the headers to be flushed; that or touching the
+  // bodyOutputStream getter.
+  response.write("");
+  dump("server-stream-download.js: monitor headers issued\n");
+
+  const alreadyCompleted = getGlobalState(MONITOR_KEY);
+  if (alreadyCompleted) {
+    completeMonitorResponse(response, alreadyCompleted);
+  } else {
+    setGlobalState(response, MONITOR_KEY);
+  }
+}
+
+const MAX_TICK_COUNT = 3000;
+const TICK_INTERVAL = 2;
+function handleStreamRequest(request, response) {
   const name = "server-stream-download";
 
   // Create some payload to send.
   let strChunk =
     'Static routes are the future of ServiceWorkers! So say we all!\n';
   while (strChunk.length < 1024) {
     strChunk += strChunk;
   }
 
   response.setHeader("Content-Disposition", `attachment; filename="${name}"`);
   response.setHeader("Content-Type", `application/octet-stream; name="${name}"`);
-  response.setHeader("Content-Length", `${strChunk.length * MAX_COUNT}`);
+  response.setHeader("Content-Length", `${strChunk.length * MAX_TICK_COUNT}`);
+  response.setStatusLine(request.httpVersion, 200, "Found");
 
   response.processAsync();
   response.write(strChunk);
+  dump("server-stream-download.js: stream headers + first payload issued\n");
 
   let count = 0;
   let intervalId;
+  function closeStream(why, message) {
+    dump("server-stream-download.js: closing stream: " + why + "\n");
+    clearInterval(intervalId);
+    response.finish();
+
+    const data = { why, message };
+    const monitorResponse = getGlobalState(MONITOR_KEY);
+    if (monitorResponse) {
+      completeMonitorResponse(monitorResponse, data);
+    } else {
+      setGlobalState(data, MONITOR_KEY);
+    }
+  }
   function tick() {
     try {
       // bound worst-case behavior.
-      if (count++ > MAX_COUNT) {
-        clearInterval(intervalId);
-        response.finish();
+      if (count++ > MAX_TICK_COUNT) {
+        closeStream("timeout", "timeout");
+        return;
       }
       response.write(strChunk);
     } catch(e) {
-      response.finish();
+      closeStream("canceled", e.message);
     }
   }
-  intervalId = setInterval(tick, 1);
+  intervalId = setInterval(tick, TICK_INTERVAL);
+}
+
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+function handleRequest(request, response) {
+  dump("server-stream-download.js: processing request for " + request.path +
+       "?" + request.queryString + "\n");
+  const query = new URLSearchParams(request.queryString);
+  if (query.has("monitor")) {
+    handleMonitorRequest(request, response);
+  } else {
+    handleStreamRequest(request, response);
+  }
 }
\ No newline at end of file
--- a/dom/workers/test/serviceworkers/download_canceled/sw_download_canceled.js
+++ b/dom/workers/test/serviceworkers/download_canceled/sw_download_canceled.js
@@ -1,75 +1,128 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // This file is derived from :bkelly's https://glitch.com/edit/#!/html-sw-stream
 
-addEventListener('install', evt => {
+addEventListener("install", evt => {
   evt.waitUntil(self.skipWaiting());
 });
 
-addEventListener('activate', function(evt) {
+addEventListener("activate", function(evt) {
   evt.waitUntil(clients.claim());
 });
 
+// Create a BroadcastChannel to notify when we have closed our streams.
+const channel = new BroadcastChannel("stream-closed");
+
+const MAX_TICK_COUNT = 3000;
+const TICK_INTERVAL = 4;
 /**
  * Generate a continuous stream of data at a sufficiently high frequency that a
- * there's a good chance of racing channel cancellation.
+ * there"s a good chance of racing channel cancellation.
  */
 function handleStream(evt, filename) {
   // Create some payload to send.
   const encoder = new TextEncoder();
   let strChunk =
-    'Static routes are the future of ServiceWorkers! So say we all!\n';
+    "Static routes are the future of ServiceWorkers! So say we all!\n";
   while (strChunk.length < 1024) {
     strChunk += strChunk;
   }
   const dataChunk = encoder.encode(strChunk);
 
   evt.waitUntil(new Promise(resolve => {
     let body = new ReadableStream({
       start: controller => {
+        const closeStream = (why) => {
+          console.log("closing stream: " + JSON.stringify(why) + "\n");
+          clearInterval(intervalId);
+          resolve();
+          // In event of error, the controller will automatically have closed.
+          if (why.why != "canceled") {
+            try {
+              controller.close();
+            } catch(ex) {
+              // If we thought we should cancel but experienced a problem,
+              // that's a different kind of failure and we need to report it.
+              // (If we didn't catch the exception here, we'd end up erroneously
+              // in the tick() method's canceled handler.)
+              channel.postMessage({
+                what: filename,
+                why: "close-failure",
+                message: ex.message,
+                ticks: why.ticks
+              });
+              return;
+            }
+          }
+          // Post prior to performing any attempt to close...
+          channel.postMessage(why);
+        };
+
         controller.enqueue(dataChunk);
         let count = 0;
         let intervalId;
         function tick() {
           try {
             // bound worst-case behavior.
-            if (count++ > 3000) {
-              clearInterval(intervalId);
-              resolve();
-              controller.close();
+            if (count++ > MAX_TICK_COUNT) {
+              closeStream({
+                what: filename, why: "timeout", message: "timeout", ticks: count
+              });
+              return;
             }
             controller.enqueue(dataChunk);
           } catch(e) {
-            resolve();
-            controller.close();
+            closeStream({
+              what: filename, why: "canceled", message: e.message, ticks: count
+            });
           }
         }
-        intervalId = setInterval(tick, 1);
+        intervalId = setInterval(tick, TICK_INTERVAL);
+        tick();
       },
     });
     evt.respondWith(new Response(body, {
       headers: {
-        'Content-Disposition': `attachment; filename="${filename}"`,
-        'Content-Type': 'application/octet-stream'
+        "Content-Disposition": `attachment; filename="${filename}"`,
+        "Content-Type": "application/octet-stream"
       }
     }));
   }));
 }
 
-function handlePassThrough(evt) {
-  evt.respondWith(fetch('server-stream-download.sjs').then(response => {
+/**
+ * Use an .sjs to generate a similar stream of data to the above, passing the
+ * response through directly.  Because we're handing off the response but also
+ * want to be able to report when cancellation occurs, we create a second,
+ * overlapping long-poll style fetch that will not finish resolving until the
+ * .sjs experiences closure of its socket and terminates the payload stream.
+ */
+function handlePassThrough(evt, filename) {
+  evt.waitUntil((async () => {
+    console.log("issuing monitor fetch request");
+    const response = await fetch("server-stream-download.sjs?monitor");
+    console.log("monitor headers received, awaiting body");
+    const data = await response.json();
+    console.log("passthrough monitor fetch completed, notifying.");
+    channel.postMessage({
+      what: filename,
+      why: data.why,
+      message: data.message
+    });
+  })());
+  evt.respondWith(fetch("server-stream-download.sjs").then(response => {
     console.log("server-stream-download.sjs Response received, propagating");
     return response;
   }));
 }
 
-addEventListener('fetch', evt => {
+addEventListener("fetch", evt => {
   console.log(`SW processing fetch of ${evt.request.url}`);
-  if (evt.request.url.indexOf('sw-stream-download') >= 0) {
-    return handleStream(evt, 'sw-stream-download');
+  if (evt.request.url.indexOf("sw-stream-download") >= 0) {
+    return handleStream(evt, "sw-stream-download");
   }
-  if (evt.request.url.indexOf('sw-passthrough-download') >= 0) {
-    return handlePassThrough(evt);
+  if (evt.request.url.indexOf("sw-passthrough-download") >= 0) {
+    return handlePassThrough(evt, "sw-passthrough-download");
   }
 })
\ No newline at end of file