Bug 1392003 - Support estimatedEndTime in DownloadItem; r=aswan
authorThomas Wisniewski <wisniewskit@gmail.com>
Sat, 19 Aug 2017 15:06:46 -0400
changeset 649608 1ba1737b4878853c1f23856b92450978b8e6c3de
parent 649607 9922fa33c5be7ad571f5f1c097befe04b13b66a8
child 649609 d04d4fdcd808a4a1f469893c5fca11c304698c5c
push id75073
push userbmo:mozilla@hocat.ca
push dateSun, 20 Aug 2017 22:21:51 +0000
reviewersaswan
bugs1392003
milestone57.0a1
Bug 1392003 - Support estimatedEndTime in DownloadItem; r=aswan MozReview-Commit-ID: 4Yzj52qI1Mz
toolkit/components/extensions/ext-downloads.js
toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -26,16 +26,18 @@ var {
 const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
                               "danger", "mime", "startTime", "endTime",
                               "estimatedEndTime", "state",
                               "paused", "canResume", "error",
                               "bytesReceived", "totalBytes",
                               "fileSize", "exists",
                               "byExtensionId", "byExtensionName"];
 
+const DOWNLOAD_DATE_FIELDS = ["startTime", "endTime", "estimatedEndTime"];
+
 // Fields that we generate onChanged events for.
 const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
                                      "error", "exists"];
 
 // From https://fetch.spec.whatwg.org/#forbidden-header-name
 const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING",
                            "ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD",
                            "CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT",
@@ -55,17 +57,24 @@ class DownloadItem {
   get url() { return this.download.source.url; }
   get referrer() { return this.download.source.referrer; }
   get filename() { return this.download.target.path; }
   get incognito() { return this.download.source.isPrivate; }
   get danger() { return "safe"; } // TODO
   get mime() { return this.download.contentType; }
   get startTime() { return this.download.startTime; }
   get endTime() { return null; } // TODO
-  get estimatedEndTime() { return null; } // TODO
+  get estimatedEndTime() {
+    // Based on the code in summarizeDownloads() in DownloadsCommon.jsm
+    if (this.download.hasProgress && this.download.speed > 0) {
+      let sizeLeft = this.download.totalBytes - this.download.currentBytes;
+      let rawTimeLeft = sizeLeft / this.download.speed;
+      return new Date(Date.now() + rawTimeLeft);
+    }
+  }
   get state() {
     if (this.download.succeeded) {
       return "complete";
     }
     if (this.download.canceled) {
       return "interrupted";
     }
     return "in_progress";
@@ -115,18 +124,20 @@ class DownloadItem {
    * @returns {object} A DownloadItem with flat properties,
    *                   suitable for cloning.
    */
   serialize() {
     let obj = {};
     for (let field of DOWNLOAD_ITEM_FIELDS) {
       obj[field] = this[field];
     }
-    if (obj.startTime) {
-      obj.startTime = obj.startTime.toISOString();
+    for (let field of DOWNLOAD_DATE_FIELDS) {
+      if (obj[field]) {
+        obj[field] = obj[field].toISOString();
+      }
     }
     return obj;
   }
 
   // When a change event fires, handlers can look at how an individual
   // field changed by comparing item.fieldname with item.prechange.fieldname.
   // After all handlers have been invoked, this gets called to store the
   // current values of all fields ahead of the next event.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -43,16 +43,23 @@ function handleRequest(request, response
       response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
       response.finish();
       return;
     }
 
     response.setStatusLine(request.httpVersion, 206, "Partial Content");
     response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
     response.write(TEST_DATA.slice(start, end + 1));
+  } else if (request.queryString.includes("stream")) {
+    response.processAsync();
+    response.setHeader("Content-Length", "10000", false);
+    response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+    setInterval(() => {
+      response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+    }, 50);
   } else {
     response.processAsync();
     response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
     response.write(TEST_DATA.slice(0, PARTIAL_LEN));
   }
 
   do_register_cleanup(() => {
     try {
@@ -212,25 +219,25 @@ function runInExtension(what, ...args) {
   extension.sendMessage(`${what}.request`, ...args);
   return extension.awaitMessage(`${what}.done`);
 }
 
 // This is pretty simplistic, it looks for a progress update for a
 // download of the given url in which the total bytes are exactly equal
 // to the given value.  Unless you know exactly how data will arrive from
 // the server (eg see interruptible.sjs), it probably isn't very useful.
-async function waitForProgress(url, bytes) {
+async function waitForProgress(url, testFn) {
   let list = await Downloads.getList(Downloads.ALL);
 
   return new Promise(resolve => {
     const view = {
       onDownloadChanged(download) {
-        if (download.source.url == url && download.currentBytes == bytes) {
+        if (download.source.url == url && testFn(download.currentBytes)) {
           list.removeView(view);
-          resolve();
+          resolve(download.currentBytes);
         }
       },
     };
     list.addView(view);
   });
 }
 
 add_task(async function setup() {
@@ -288,17 +295,17 @@ add_task(async function test_events() {
 
 add_task(async function test_cancel() {
   let url = getInterruptibleUrl();
   do_print(url);
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -343,16 +350,17 @@ add_task(async function test_cancel() {
   equal(msg.status, "success", "got onChanged events corresponding to cancel()");
 
   msg = await runInExtension("search", {error: "USER_CANCELED"});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].id, id, "download.id is correct");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, false, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, false, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 
   msg = await runInExtension("pause", id);
   equal(msg.status, "error", "cannot pause a canceled download");
 
@@ -361,17 +369,17 @@ add_task(async function test_cancel() {
 });
 
 add_task(async function test_pauseresume() {
   let url = getInterruptibleUrl();
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -410,16 +418,17 @@ add_task(async function test_pauseresume
   equal(msg.status, "success", "got onChanged event corresponding to pause");
 
   msg = await runInExtension("search", {paused: true});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].id, id, "download.id is correct");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, true, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, true, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 
   msg = await runInExtension("search", {error: "USER_CANCELED"});
   equal(msg.status, "success", "search() succeeded");
@@ -468,16 +477,17 @@ add_task(async function test_pauseresume
   ]);
   equal(msg.status, "success", "got onChanged events for resume and complete");
 
   msg = await runInExtension("search", {id});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].state, "complete", "download.state is correct");
   equal(msg.result[0].paused, false, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, false, "download.canResume is correct");
   equal(msg.result[0].error, null, "download.error is correct");
   equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, true, "download.exists is correct");
 
   msg = await runInExtension("pause", id);
   equal(msg.status, "error", "cannot pause a completed download");
@@ -487,17 +497,17 @@ add_task(async function test_pauseresume
 });
 
 add_task(async function test_pausecancel() {
   let url = getInterruptibleUrl();
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -536,16 +546,17 @@ add_task(async function test_pausecancel
   equal(msg.status, "success", "got onChanged event corresponding to pause");
 
   msg = await runInExtension("search", {paused: true});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].id, id, "download.id is correct");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, true, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, true, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 
   msg = await runInExtension("search", {error: "USER_CANCELED"});
   equal(msg.status, "success", "search() succeeded");
@@ -573,16 +584,17 @@ add_task(async function test_pausecancel
   ]);
   equal(msg.status, "success", "got onChanged event for cancel");
 
   msg = await runInExtension("search", {id});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, false, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, false, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 });
 
 add_task(async function test_pause_resume_cancel_badargs() {
   let BAD_ID = 1000;
@@ -633,17 +645,17 @@ add_task(async function test_file_remova
 });
 
 add_task(async function test_removal_of_incomplete_download() {
   let url = getInterruptibleUrl();
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -852,11 +864,37 @@ add_task(async function test_getFileIcon
 
   msg = await runInExtension("getFileIcon", id, {size: 128});
   equal(msg.status, "error", "getFileIcon() fails");
   ok(msg.errmsg.includes("Error processing size"), "size is too big");
 
   webNav.close();
 });
 
+add_task(async function test_estimatedendtime() {
+  // Note we are not testing the actual value calculation of estimatedEndTime,
+  // only whether it is null/non-null at the appropriate times.
+
+  let url = `${getInterruptibleUrl()}&stream=1`;
+  let msg = await runInExtension("download", {url});
+  equal(msg.status, "success", "download() succeeded");
+  const id = msg.result;
+
+  let previousBytes = await waitForProgress(url, bytes => bytes > 0);
+  await waitForProgress(url, bytes => bytes > previousBytes);
+
+  msg = await runInExtension("search", {id});
+  equal(msg.status, "success", "search() succeeded");
+  equal(msg.result.length, 1, "search() found 1 download");
+  ok(msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+  ok(msg.result[0].bytesReceived > 0, "download.bytesReceived is correct");
+
+  msg = await runInExtension("cancel", id);
+
+  msg = await runInExtension("search", {id});
+  equal(msg.status, "success", "search() succeeded");
+  equal(msg.result.length, 1, "search() found 1 download");
+  ok(!msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+});
+
 add_task(async function cleanup() {
   await extension.unload();
 });