Bug 572520: step 9, test asynchronous notification for regular loads and channel loads on static, animated, and 404 images. r=jrmuizel,bholley
authorJoe Drew <joe@drew.ca>
Wed, 28 Jul 2010 14:52:59 -0700
changeset 48324 b8b62b351c09249a97e2d1aafdfb1cc89e00782c
parent 48323 3da4f3cf80e712e5105ee6ccbbea900f7487c266
child 48331 6781d298f7042ff203d7ee0d17508f471bd254fc
push id14681
push userjdrew@mozilla.com
push dateWed, 28 Jul 2010 21:53:45 +0000
treeherdermozilla-central@b8b62b351c09 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjrmuizel, bholley
bugs572520
milestone2.0b3pre
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 572520: step 9, test asynchronous notification for regular loads and channel loads on static, animated, and 404 images. r=jrmuizel,bholley
modules/libpr0n/test/unit/async_load_tests.js
modules/libpr0n/test/unit/image_load_helpers.js
modules/libpr0n/test/unit/test_async_notification.js
modules/libpr0n/test/unit/test_async_notification_404.js
modules/libpr0n/test/unit/test_async_notification_animated.js
new file mode 100644
--- /dev/null
+++ b/modules/libpr0n/test/unit/async_load_tests.js
@@ -0,0 +1,206 @@
+/*
+ * Test to ensure that image loading/decoding notifications are always
+ * delivered async, and in the order we expect.
+ *
+ * Must be included from a file that has a uri of the image to test defined in
+ * var uri.
+ */
+
+do_load_httpd_js();
+
+var server = new nsHttpServer();
+server.registerDirectory("/", do_get_file(''));
+server.registerContentType("sjs", "sjs");
+server.start(8088);
+
+load('image_load_helpers.js');
+
+// Return a closure that holds on to the listener from the original
+// imgIRequest, and compares its results to the cloned one.
+function getCloneStopCallback(original_listener)
+{
+  return function cloneStop(listener) {
+    do_check_eq(original_listener.state, listener.state);
+
+    // Sanity check to make sure we didn't accidentally use the same listener
+    // twice.
+    do_check_neq(original_listener, listener);
+    do_test_finished();
+  }
+}
+
+// Make sure that cloned requests get all the same callbacks as the original,
+// but they aren't synchronous right now.
+function checkClone(other_listener, aRequest)
+{
+  do_test_pending();
+
+  // For as long as clone notification is synchronous, we can't test the clone state reliably.
+  var listener = new ImageListener(null, function(foo, bar) { do_test_finished(); } /*getCloneStopCallback(other_listener)*/);
+  listener.synchronous = false;
+  var clone = aRequest.clone(listener);
+}
+
+// Ensure that all the callbacks were called on aRequest.
+function checkAllCallbacks(listener, aRequest)
+{
+  do_check_eq(listener.state, ALL_BITS);
+
+  do_test_finished();
+}
+
+function secondLoadDone(oldlistener, aRequest)
+{
+  do_test_pending();
+
+  try {
+    var staticrequest = aRequest.getStaticRequest();
+
+    // For as long as clone notification is synchronous, we can't test the
+    // clone state reliably.
+    var listener = new ImageListener(null, checkAllCallbacks);
+    listener.synchronous = false;
+    var staticrequestclone = staticrequest.clone(listener);
+  } catch(e) {
+    // We can't create a static request. Most likely the request we started
+    // with didn't load successfully.
+    do_test_finished();
+  }
+
+  run_loadImageWithChannel_tests();
+
+  do_test_finished();
+}
+
+// Load the request a second time. This should come from the image cache, and
+// therefore would be at most risk of being served synchronously.
+function checkSecondLoad()
+{
+  do_test_pending();
+
+  var loader = Cc["@mozilla.org/image/loader;1"].getService(Ci.imgILoader);
+  var listener = new ImageListener(checkClone, secondLoadDone);
+  var req = loader.loadImage(uri, null, null, null, listener, null, 0, null, null, null);
+  listener.synchronous = false;
+}
+
+function firstLoadDone(oldlistener, aRequest)
+{
+  checkSecondLoad(uri);
+
+  do_test_finished();
+}
+
+// Return a closure that allows us to check the stream listener's status when the
+// image starts loading.
+function getChannelLoadImageStartCallback(streamlistener)
+{
+  return function channelLoadStart(imglistener, aRequest) {
+    // We must not have received any status before we get this start callback.
+    // If we have, we've broken people's expectations by delaying events from a
+    // channel we were given.
+    do_check_eq(streamlistener.requestStatus, 0);
+
+    checkClone(imglistener, aRequest);
+  }
+}
+
+// Return a closure that allows us to check the stream listener's status when the
+// image finishes loading.
+function getChannelLoadImageStopCallback(streamlistener, next)
+{
+  return function channelLoadStop(imglistener, aRequest) {
+    // We absolutely must not get imgIDecoderObserver::onStopRequest after
+    // nsIRequestObserver::onStopRequest has fired. If we do that, we've broken
+    // people's expectations by delaying events from a channel we were given.
+    do_check_eq(streamlistener.requestStatus & STOP_REQUEST, 0);
+
+    next();
+
+    do_test_finished();
+  }
+}
+
+// Load the request a second time. This should come from the image cache, and
+// therefore would be at most risk of being served synchronously.
+function checkSecondChannelLoad()
+{
+  do_test_pending();
+
+  var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);  
+  var channel = ioService.newChannelFromURI(uri);
+  var channellistener = new ChannelListener();
+  channel.asyncOpen(channellistener, null);
+
+  var loader = Cc["@mozilla.org/image/loader;1"].getService(Ci.imgILoader);
+  var listener = new ImageListener(getChannelLoadImageStartCallback(channellistener),
+                                   getChannelLoadImageStopCallback(channellistener,
+                                                                   all_done_callback));
+  var outlistener = {};
+  var req = loader.loadImageWithChannel(channel, listener, null, outlistener);
+  channellistener.outputListener = outlistener.value;
+
+  listener.synchronous = false;
+}
+
+function run_loadImageWithChannel_tests()
+{
+  // To ensure we're testing what we expect to, clear the content image cache
+  // between test runs.
+  var loader = Cc["@mozilla.org/image/loader;1"].getService(Ci.imgILoader);
+  loader.QueryInterface(Ci.imgICache);
+  loader.clearCache(false);
+
+  do_test_pending();
+
+  var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);  
+  var channel = ioService.newChannelFromURI(uri);
+  var channellistener = new ChannelListener();
+  channel.asyncOpen(channellistener, null);
+
+  var loader = Cc["@mozilla.org/image/loader;1"].getService(Ci.imgILoader);
+  var listener = new ImageListener(getChannelLoadImageStartCallback(channellistener),
+                                   getChannelLoadImageStopCallback(channellistener,
+                                                                   checkSecondChannelLoad));
+  var outlistener = {};
+  var req = loader.loadImageWithChannel(channel, listener, null, outlistener);
+  channellistener.outputListener = outlistener.value;
+
+  listener.synchronous = false;
+}
+
+function all_done_callback()
+{
+  server.stop(function() { do_test_finished(); });
+}
+
+function startImageCallback(otherCb)
+{
+  return function(listener, request)
+  {
+    var loader = Cc["@mozilla.org/image/loader;1"].getService(Ci.imgILoader);
+
+    // Make sure we can load the same image immediately out of the cache.
+    do_test_pending();
+    var listener2 = new ImageListener(null, function(foo, bar) { do_test_finished(); });
+    var req2 = loader.loadImage(uri, null, null, null, listener2, null, 0, null, null, null);
+    listener2.synchronous = false;
+
+    // Now that we've started another load, chain to the callback.
+    otherCb(listener, request);
+  }
+}
+
+function run_test()
+{
+  var loader = Cc["@mozilla.org/image/loader;1"].getService(Ci.imgILoader);
+
+  do_test_pending();
+  var listener = new ImageListener(startImageCallback(checkClone), firstLoadDone);
+  var req = loader.loadImage(uri, null, null, null, listener, null, 0, null, null, null);
+
+  // Ensure that we don't cause any mayhem when we lock an image.
+  req.lockImage();
+
+  listener.synchronous = false;
+}
new file mode 100644
--- /dev/null
+++ b/modules/libpr0n/test/unit/image_load_helpers.js
@@ -0,0 +1,143 @@
+/*
+ * Helper structures to track callbacks from image and channel loads.
+ */
+
+// One bit per callback that imageListener below implements. Stored in
+// ImageListener.state.
+// START_REQUEST and STOP_REQUEST are also reused by ChannelListener, and
+// stored in ChannelListener.requestStatus.
+const START_REQUEST = 0x01;
+const START_DECODE = 0x02;
+const START_CONTAINER = 0x04;
+const START_FRAME = 0x08;
+const STOP_FRAME = 0x10;
+const STOP_CONTAINER = 0x20;
+const STOP_DECODE = 0x40;
+const STOP_REQUEST = 0x80;
+const ALL_BITS = 0xFF;
+
+// An implementation of imgIDecoderObserver with the ability to call specified
+// functions on onStartRequest and onStopRequest.
+function ImageListener(start_callback, stop_callback)
+{
+  this.onStartRequest = function onStartRequest(aRequest)
+  {
+    do_check_false(this.synchronous);
+
+    this.state |= START_REQUEST;
+
+    if (this.start_callback)
+      this.start_callback(this, aRequest);
+  }
+  this.onStartDecode = function onStartDecode(aRequest)
+  {
+    do_check_false(this.synchronous);
+
+    this.state |= START_DECODE;
+  }
+  this.onStartContainer = function onStartContainer(aRequest, aContainer)
+  {
+    do_check_false(this.synchronous);
+
+    this.state |= START_CONTAINER;
+  }
+  this.onStartFrame = function onStartFrame(aRequest, aFrame)
+  {
+    do_check_false(this.synchronous);
+
+    this.state |= START_FRAME;
+  }
+  this.onStopFrame = function onStopFrame(aRequest, aFrame)
+  {
+    do_check_false(this.synchronous);
+
+    this.state |= STOP_FRAME;
+  }
+  this.onStopContainer = function onStopContainer(aRequest, aContainer)
+  {
+    do_check_false(this.synchronous);
+
+    this.state |= STOP_CONTAINER;
+  }
+  this.onStopDecode = function onStopDecode(aRequest, status, statusArg)
+  {
+    do_check_false(this.synchronous);
+
+    this.state |= STOP_DECODE;
+  }
+  this.onStopRequest = function onStopRequest(aRequest, aIsLastPart)
+  {
+    do_check_false(this.synchronous);
+
+    // onStopDecode must always be called before, and with, onStopRequest. See
+    // imgRequest::OnStopDecode for more information.
+    do_check_true(!!(this.state & STOP_DECODE));
+
+    // We have to cancel the request when we're done with it to break any
+    // reference loops!
+    aRequest.cancel(0);
+
+    this.state |= STOP_REQUEST;
+
+    if (this.stop_callback)
+      this.stop_callback(this, aRequest);
+  }
+
+  // Initialize the synchronous flag to true to start. This must be set to
+  // false before exiting to the event loop!
+  this.synchronous = true;
+
+  // A function to call when onStartRequest is called.
+  this.start_callback = start_callback;
+
+  // A function to call when onStopRequest is called.
+  this.stop_callback = stop_callback;
+
+  // The image load/decode state.
+  // A bitfield that tracks which callbacks have been called. Takes the bits
+  // defined above.
+  this.state = 0;
+}
+
+function NS_FAILED(val)
+{
+  return !!(val & 0x80000000);
+}
+
+function ChannelListener()
+{
+  this.onStartRequest = function onStartRequest(aRequest, aContext)
+  {
+    if (this.outputListener)
+      this.outputListener.onStartRequest(aRequest, aContext);
+
+    this.requestStatus |= START_REQUEST;
+  }
+
+  this.onDataAvailable = function onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount)
+  {
+    if (this.outputListener)
+      this.outputListener.onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount);
+  }
+
+  this.onStopRequest = function onStopRequest(aRequest, aContext, aStatusCode)
+  {
+    if (this.outputListener)
+      this.outputListener.onStopRequest(aRequest, aContext, aStatusCode);
+
+    // If we failed (or were canceled - failure is implied if canceled),
+    // there's no use tracking our state, since it's meaningless.
+    if (NS_FAILED(aStatusCode))
+      this.requestStatus = 0;
+    else
+      this.requestStatus |= STOP_REQUEST;
+  }
+
+  // A listener to pass the notifications we get to.
+  this.outputListener = null;
+
+  // The request's status. A bitfield that holds one or both of START_REQUEST
+  // and STOP_REQUEST, according to which callbacks have been called on the
+  // associated request.
+  this.requestStatus = 0;
+}
new file mode 100644
--- /dev/null
+++ b/modules/libpr0n/test/unit/test_async_notification.js
@@ -0,0 +1,10 @@
+/*
+ * Test for asynchronous image load/decode notifications in the case that the image load works.
+ */
+
+// A simple 3x3 png; rows go red, green, blue. Stolen from the PNG encoder test.
+var pngspec = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=";
+var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+var uri = ioService.newURI(pngspec, null, null);
+
+load('async_load_tests.js');
new file mode 100644
--- /dev/null
+++ b/modules/libpr0n/test/unit/test_async_notification_404.js
@@ -0,0 +1,8 @@
+/*
+ * Test to ensure that load/decode notifications are delivered completely and
+ * asynchronously when dealing with a file that's a 404.
+ */
+var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+var uri = ioService.newURI("http://localhost:8088/async-notification-never-here.jpg", null, null);
+
+load('async_load_tests.js');
new file mode 100644
--- /dev/null
+++ b/modules/libpr0n/test/unit/test_async_notification_animated.js
@@ -0,0 +1,14 @@
+/*
+ * Test for asynchronous image load/decode notifications in the case that the
+ * image load works, but for an animated image.
+ *
+ * If this fails because a request wasn't cancelled, it's possible that
+ * imgContainer::ExtractFrame didn't set the new image's status correctly.
+ */
+
+// transparent-animation.gif from the gif reftests.
+var spec = "data:image/gif;base64,R0lGODlhZABkAIABAP8AAP///yH5BAkBAAEALAAAAABLAGQAAAK8jI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8YhMKpchgPMJjUqnVOipis1ir9qul+sNV8HistVkTj/JajG7/UXDy+95tm4fy/NdPF/q93dWIqgVWAhwWKgoyPjnyAeZJ2lHOWcJh9mmqcaZ5mkGSreHOCXqRloadRrGGkeoapoa6+TaN0tra4gbq3vHq+q7BVwqrMeEnKy8zNzs/AwdLT1NXW19jZ1tUgAAIfkECQEAAQAsAAAAADQAZAAAArCMj6nL7Q+jnLTai7PevPsPhuJIluaJpurKtu4Lx/JM1/aN5/rO9/7vAAiHxKLxiCRCkswmc+mMSqHSapJqzSof2u4Q67WCw1MuOTs+N9Pqq7kdZcON8vk2aF+/88g6358HaCc4Rwhn2IaopnjGSOYYBukl2UWpZYm2x0enuXnX4NnXGQqAKTYaalqlWoZH+snwWsQah+pJ64Sr5ypbCvQLHCw8TFxsfIycrLzM3PxQAAAh+QQJAQABACwAAAAAGwBkAAACUIyPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gTE8kzX9o3n+s73/g8MCofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvdrfYnH5LL5jE6r16sCADs=";
+var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+var uri = ioService.newURI(spec, null, null);
+
+load('async_load_tests.js');