Bug 1443942 - Rewrite test_mediarecorder_principals. r=bryce
authorChris Pearce <cpearce@mozilla.com>
Thu, 05 Apr 2018 13:35:14 +1200
changeset 412500 8b3f66ef0cc427ba2afca030b31194fb5b7aa95b
parent 412499 f2306039790583a2c1f75888010d044600d75f82
child 412501 2a39d6a9a949dac9c9cba0a9dfc1bd93de88c79e
push id101938
push usercpearce@mozilla.com
push dateTue, 10 Apr 2018 03:44:24 +0000
treeherdermozilla-inbound@41039837009c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbryce
bugs1443942
milestone61.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 1443942 - Rewrite test_mediarecorder_principals. r=bryce I changed this test earlier in this set of commits to use midflight-redirect.sjs so that we get more reliable and predictable cross origin redirects during the download. Unfortunately this test now times out on Windows. This test times out on Windows because midflight-redirect.sjs redirects at 1/4 through the resource, whereas this test expects to be able to play through to 1/5 through the resource, and on Windows that seems to be not reached during playback. This is likely due to decode latency being higher on Windows. On top of that, the test's first case can sometimes call MediaRecorder.start() before the redirect has happened, and before the principal has changed, and so start() doesn't throw a SecurityError as expected, and the test intermittently fails. Additionally, the test's code could be clearer if we used async/await. So rewrite the test to use async/await, and take advantage of midflight-redirect.sjs's redirect being more predictable than the old dynamic_redirect.sjs. Basically, we can be careful to wait for either "loadedmetadata" or "error" on the media element in order to be more confident the redirect has or hasn't happened yet. We still can't be 100% sure that the redirect won't have already happened by the time our "loadedmetadata" handlers run. It's quite possible that the download has reached 1/4 through the resource by the time the loadedmetadata handler has run, so we need to handle the "error" and "loadedmetadata" events racing. MozReview-Commit-ID: 8plMjkXgjYt
dom/media/test/AutoplayTestUtils.js
dom/media/test/background_video.js
dom/media/test/file_autoplay_policy_activation_frame.html
dom/media/test/manifest.js
dom/media/test/test_mediarecorder_principals.html
--- a/dom/media/test/AutoplayTestUtils.js
+++ b/dom/media/test/AutoplayTestUtils.js
@@ -11,26 +11,16 @@ function playAndPostResult(muted, parent
         parent_window.postMessage({played: true}, "*");
       },
       () => {
         parent_window.postMessage({played: false}, "*");
       }
     );
 }
 
-function nextEvent(eventTarget, eventName) {
-  return new Promise(function(resolve, reject) {
-    let f = function(event) {
-      eventTarget.removeEventListener(eventName, f, false);
-      resolve(event);
-    };
-    eventTarget.addEventListener(eventName, f, false);
-  });
-}
-
 function nextWindowMessage() {
   return nextEvent(window, "message");
 }
 
 function log(msg) {
   var log_pane = document.body;
   log_pane.appendChild(document.createTextNode(msg));
   log_pane.appendChild(document.createElement("br"));
--- a/dom/media/test/background_video.js
+++ b/dom/media/test/background_video.js
@@ -9,32 +9,16 @@
 function startTest(test) {
   info(test.desc);
   SimpleTest.waitForExplicitFinish();
   SpecialPowers.pushPrefEnv({ 'set': test.prefs }, () => {
     manager.runTests(test.tests, test.runTest);
   });
 }
 
-/**
- * @param {HTMLMediaElement} video target of interest.
- * @param {string} eventName the event to wait on.
- * @returns {Promise} A promise that is resolved when event happens.
- */
-function nextEvent(video, eventName) {
-  return new Promise(function (resolve, reject) {
-    let f = function (event) {
-      ok(true, `${video.token} ${eventName}.`);
-      video.removeEventListener(eventName, f, false);
-      resolve(event);
-    };
-    video.addEventListener(eventName, f, false);
-  });
-}
-
 function nextVideoEnded(video) {
   return nextEvent(video, 'ended');
 }
 
 function nextVideoPlaying(video) {
   return nextEvent(video, 'playing');
 }
 
--- a/dom/media/test/file_autoplay_policy_activation_frame.html
+++ b/dom/media/test/file_autoplay_policy_activation_frame.html
@@ -1,12 +1,13 @@
 <!DOCTYPE HTML>
 <html>
   <head>
     <title>Autoplay policy frame</title>
+    <script type="text/javascript" src="manifest.js"></script>
     <script type="text/javascript" src="AutoplayTestUtils.js"></script>
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
     <style>
       video {
         width: 50%;
         height: 50%;
       }
     </style>
--- a/dom/media/test/manifest.js
+++ b/dom/media/test/manifest.js
@@ -1586,16 +1586,31 @@ function once(target, name, cb) {
     }, {once: true});
   });
   if (cb) {
     p.then(cb);
   }
   return p;
 }
 
+/**
+ * @param {HTMLMediaElement} video target of interest.
+ * @param {string} eventName the event to wait on.
+ * @returns {Promise} A promise that is resolved when event happens.
+ */
+function nextEvent(video, eventName) {
+  return new Promise(function (resolve, reject) {
+    let f = function (event) {
+      video.removeEventListener(eventName, f, false);
+      resolve(event);
+    };
+    video.addEventListener(eventName, f, false);
+  });
+}
+
 function TimeStamp(token) {
   function pad(x) {
     return (x < 10) ? "0" + x : x;
   }
   var now = new Date();
   var ms = now.getMilliseconds();
   var time = "[" +
              pad(now.getHours()) + ":" +
--- a/dom/media/test/test_mediarecorder_principals.html
+++ b/dom/media/test/test_mediarecorder_principals.html
@@ -10,109 +10,123 @@ https://bugzilla.mozilla.org/show_bug.cg
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <script type="text/javascript" src="manifest.js"></script>
 </head>
 <body>
 <div>
   <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1018299">Test for MediaRecorder Principal Handling</a>
 </div>
 
-<video id="v1" preload="metadata"></video>
-<video id="v2" preload="metadata"></video>
-
 <pre id="test">
 <script type="text/javascript">
 SimpleTest.waitForExplicitFinish();
 
 let throwOutside = e => setTimeout(() => { throw e; });
 
-// Generate a random key. The first load with that key will return
-// data, the second and subsequent loads with that key will return a redirect
-// to a different origin ('localhost:8888' will be redirected to 'example.org',
-// and 'example.org' will be redirected to 'localhost:8888').
-// Loading data from two different origins should be detected by the media
-// cache and result in a null principal so that the MediaRecorder usages below
-// fail.
-// This test relies on that preloading the metadata then forcing another load
-// via video.load() will result in two requests taking place to retreive the
-// resource.
-let key = Math.floor(Math.random()*100000000);
-let interval;
+// Loading data from a resource that changes origins while streaming should
+// be detected by the media cache and result in a null principal so that the
+// MediaRecorder usages below fail.
 
-function testPrincipals(resource) {
+// This test relies on midflight-redirect.sjs returning the the first quarter of
+// the resource as a byte range response, and then hanging up, and when Firefox
+// requests the remainder midflight-redirect.sjs serves a redirect to another origin.
+
+async function testPrincipals(resource) {
   if (!resource) {
     todo(false, "No types supported");
     return;
   }
-  // First test: Load file from same-origin first, then get redirected to
-  // another origin before attempting to record stream.
-  let video = document.getElementById("v1");
+  await testPrincipals1(resource);
+  await testPrincipals2(resource);
+}
+
+function makeVideo() {
+  let video = document.createElement("video");
+  video.preload = "metadata";
+  video.controls = true;
+  document.body.appendChild(video);
+  return video;
+}
+
+// First test: Load file from same-origin first, then get redirected to
+// another origin before attempting to record stream.
+async function testPrincipals1(resource) {
+  let video = makeVideo();
   video.src =
-      "http://mochi.test:8888/tests/dom/media/test/midflight-redirect.sjs?key=v1_" +
-      key + "&resource=" + resource.name + "&type=" + resource.type;
-  return new Promise(resolve => video.onloadedmetadata = resolve).then(() => {
-    video.load();
-    video.play();
-    interval = setInterval(() => info("video.currentTime = "+ video.currentTime), 1000);
+      "http://mochi.test:8888/tests/dom/media/test/midflight-redirect.sjs" +
+      "?resource=" + resource.name + "&type=" + resource.type;
+
+  let errorBarrier = once(video, "error");
+  // Wait for the video to load to metadata. We can then start capturing.
+  // Must also handle the download bursting and hitting the error before we
+  // reach loadedmetadata. Normally we reach loadedmetadata first, but
+  // rarely we hit the redirect first.
+  await Promise.race([once(video, "loadedmetadata"), errorBarrier]);
+
+  let rec = new MediaRecorder(video.mozCaptureStreamUntilEnded());
+  video.play();
+
+  // Wait until we hit a playback error. This means our download has hit the redirect.
+  await errorBarrier;
+
+  // Try to record, it should be blocked with a security error.
+  try {
+    rec.start();
+    ok(false, "mediaRecorder.start() must throw SecurityError, but didn't throw at all");
+  } catch (ex) {
+    is(ex.name, "SecurityError", "mediaRecorder.start() must throw SecurityError");
+  }
+  removeNodeAndSource(video);
+}
 
-    let msg = "mediaRecorder.start() must throw SecurityError";
-    return new Promise(resolve => video.onplaying = resolve)
-    .then(() => waitUntil(() => video.currentTime > resource.duration / 5))
-    // Test failure of the next step only, so "catch-bypass" any errors above.
-    .then(() => Promise.resolve()
-      .then(() => new MediaRecorder(video.mozCaptureStreamUntilEnded()).start())
-      .then(() => ok(false, msg), e => is(e.name, "SecurityError", msg)), 0)
-    .then(() => clearInterval(interval));
-  })
-  .then(() => {
-    // Second test: Load file from same-origin first, but record ASAP, before
-    // getting redirected to another origin.
-    let video = document.getElementById("v2");
-    video.src =
-        "http://mochi.test:8888/tests/dom/media/test/midflight-redirect.sjs?key=v2_" +
-        key + "&resource=" + resource.name + "&type=" + resource.type;
-    let rec, hasStopped, hasEnded = new Promise(r => video.onended = r);
-    let data = [];
+// Second test: Load file from same-origin first, but record ASAP, before
+// getting redirected to another origin.
+async function testPrincipals2(resource) {
+  let video = makeVideo();
+  video.src =
+      "http://mochi.test:8888/tests/dom/media/test/midflight-redirect.sjs" +
+      "?resource=" + resource.name + "&type=" + resource.type;
+
+  // Wait for the video to load to metadata. We can then start capturing.
+  // Must also handle the download bursting and hitting the error before we
+  // reach loadedmetadata. Normally we reach loadedmetadata first, but
+  // rarely we hit the redirect first.
+  await Promise.race([once(video, "loadedmetadata"), once(video, "error")]);
+
+  let ended = false;
+  once(video, "ended", () => ended = true);
 
-    let msgNoThrow = "mediaRecorder.start() should not throw here";
-    let msgSecErr = "mediaRecorder.onerror must fire SecurityError";
-    let msgOnStop = "mediaRecorder.onstop must also have fired";
-    return new Promise(resolve => video.onloadedmetadata = resolve).then(() => {
-      rec = new MediaRecorder(video.mozCaptureStreamUntilEnded());
-      rec.ondataavailable = e => data.push(e.data);
-      rec.start();
-      video.load();
-      hasStopped = new Promise(resolve => rec.onstop = resolve);
-      video.play();
-    })
-    .then(() => ok(true, msgNoThrow), e => is(e.error.name, null, msgNoThrow))
-    .then(() => Promise.race([
-      new Promise((_, reject) => rec.onerror = e => reject(e.error)),
-      hasEnded
-    ]))
-    .then(() => ok(false, msgSecErr), e => {
-      is(e.name, "SecurityError", msgSecErr);
-      ok(e.stack.includes('test_mediarecorder_principals.html'),
-      'Events fired from onerror should include an error with a stack trace indicating ' +
-      'an error in this test');
-    })
-    .then(() => Promise.race([hasStopped, hasEnded.then(() => Promise.reject())]))
-    .then(() => ok(true, msgOnStop), e => ok(false, msgOnStop))
-    .then(() => clearInterval(interval));
-  });
+  // Start capturing. It should work.
+  let rec;
+  let errorBarrier;
+  try {
+    rec = new MediaRecorder(video.mozCaptureStreamUntilEnded());
+    errorBarrier = nextEvent(rec, "error");
+    rec.start();
+    ok(true, "mediaRecorder.start() should not throw here, and didn't");
+  } catch (ex) {
+    ok(false, "mediaRecorder.start() unexpectedly threw " + ex.name + " (" + ex.message + ")");
+  }
+
+  // Play the video, this should result in a SecurityError on the recorder.
+  let hasStopped = once(rec, "stop");
+  video.play();
+  let error = (await errorBarrier).error;
+  is(error.name, "SecurityError", "mediaRecorder.onerror must fire SecurityError");
+  ok(error.stack.includes('test_mediarecorder_principals.html'),
+    'Events fired from onerror should include an error with a stack trace indicating ' +
+    'an error in this test');
+  is(ended, false, "Playback should not have reached end");
+  await hasStopped;
+  is(ended, false, "Playback should definitely not have reached end");
+
+  removeNodeAndSource(video);
 }
 
 testPrincipals({ name:"pixel_aspect_ratio.mp4", type:"video/mp4", duration:28 })
-.catch(e => throwOutside(e))
-.then(() => SimpleTest.finish())
-.catch(e => throwOutside(e));
-
-let stop = stream => stream.getTracks().forEach(track => track.stop());
-let wait = ms => new Promise(resolve => setTimeout(resolve, ms));
-let waitUntil = f => new Promise(resolve => {
-  let ival = setInterval(() => f() && resolve(clearInterval(ival)), 100);
-});
+  .catch(e => throwOutside(e))
+  .then(() => SimpleTest.finish());
 
 </script>
 </pre>
 
 </body>
 </html>