Bug 1443942 - Rewrite test_mediarecorder_principals. r?bryce draft
authorChris Pearce <cpearce@mozilla.com>
Thu, 05 Apr 2018 13:35:14 +1200
changeset 779069 25c4cb99882558fe2d231a77baf9259c6c929d41
parent 779068 1e9c6921514f8bf4166607f524d65ae27c0e0f0f
child 779070 a1cbebb53b8d403a433bc887d0b742e1c123976f
push id105647
push userbmo:cpearce@mozilla.com
push dateSun, 08 Apr 2018 23:13:55 +0000
reviewersbryce
bugs1443942
milestone61.0a1
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/test_mediarecorder_principals.html
--- a/dom/media/test/test_mediarecorder_principals.html
+++ b/dom/media/test/test_mediarecorder_principals.html
@@ -10,109 +10,128 @@ 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.
+
+// Note: the URL contains a random element to ensure the resources aren't served
+// from Necko's cache, which would avoiding hitting the SJS and so avoiding the redirect.
+
+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();
+  let key = Math.floor(Math.random()*100000000);
   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);
+
+  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;
 
-    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 = [];
+  // 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);
+}
+
+// Second test: Load file from same-origin first, but record ASAP, before
+// getting redirected to another origin.
+async function testPrincipals2(resource) {
+  let video = makeVideo();
+  let key = Math.floor(Math.random()*100000000);
+  video.src =
+      "http://mochi.test:8888/tests/dom/media/test/midflight-redirect.sjs?key=v2_" +
+      key + "&resource=" + resource.name + "&type=" + resource.type;
 
-    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));
-  });
+  // 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);
+
+  // Start capturing. It should work.
+  let rec;
+  let errorBarrier;
+  try {
+    rec = new MediaRecorder(video.mozCaptureStreamUntilEnded());
+    errorBarrier = once(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;
+  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>