1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80: */
3 /* Any copyright is dedicated to the Public Domain.
4 * http://creativecommons.org/publicdomain/zero/1.0/ */
7 * This file tests components that implement nsIBackgroundFileSaver.
10 ////////////////////////////////////////////////////////////////////////////////
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
15 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
16 "resource://gre/modules/FileUtils.jsm");
17 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
18 "resource://gre/modules/NetUtil.jsm");
19 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
20 "resource://gre/modules/Promise.jsm");
21 XPCOMUtils.defineLazyModuleGetter(this, "Task",
22 "resource://gre/modules/Task.jsm");
24 const BackgroundFileSaverOutputStream = Components.Constructor(
25 "@mozilla.org/network/background-file-saver;1?mode=outputstream",
26 "nsIBackgroundFileSaver");
28 const BackgroundFileSaverStreamListener = Components.Constructor(
29 "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
30 "nsIBackgroundFileSaver");
32 const StringInputStream = Components.Constructor(
33 "@mozilla.org/io/string-input-stream;1",
34 "nsIStringInputStream",
37 const REQUEST_SUSPEND_AT = 1024 * 1024 * 4;
38 const TEST_DATA_SHORT = "This test string is written to the file.";
39 const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt";
40 const TEST_FILE_NAME_2 = "test-backgroundfilesaver-2.txt";
41 const TEST_FILE_NAME_3 = "test-backgroundfilesaver-3.txt";
43 // A map of test data length to the expected SHA-256 hashes
44 const EXPECTED_HASHES = {
46 0 : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
48 40 : "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192",
49 // TEST_DATA_SHORT + TEST_DATA_SHORT
50 80 : "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58",
52 8388608 : "e3611a47714c42bdf326acfb2eb6ed9fa4cca65cb7d7be55217770a5bf5e7ff0",
53 // TEST_DATA_LONG + TEST_DATA_LONG
54 16777216 : "03a0db69a30140f307587ee746a539247c181bafd85b85c8516a3533c7d9ea1d"
57 const gTextDecoder = new TextDecoder();
59 // Generate a long string of data in a moderately fast way.
60 const TEST_256_CHARS = new Array(257).join("-");
61 const DESIRED_LENGTH = REQUEST_SUSPEND_AT * 2;
62 const TEST_DATA_LONG = new Array(1 + DESIRED_LENGTH / 256).join(TEST_256_CHARS);
63 do_check_eq(TEST_DATA_LONG.length, DESIRED_LENGTH);
66 * Returns a reference to a temporary file. If the file is then created, it
67 * will be removed when tests in this file finish.
69 function getTempFile(aLeafName) {
70 let file = FileUtils.getFile("TmpD", [aLeafName]);
71 do_register_cleanup(function GTF_cleanup() {
80 * Helper function for converting a binary blob to its hex equivalent.
83 * String possibly containing non-printable chars.
84 * @return A hex-encoded string.
88 for (var i = 0; i < str.length; i++) {
89 hex += ('0' + str.charCodeAt(i).toString(16)).slice(-2);
95 * Ensures that the given file contents are equal to the given string.
98 * nsIFile whose contents should be verified.
99 * @param aExpectedContents
100 * String containing the octets that are expected in the file.
103 * @resolves When the operation completes.
106 function promiseVerifyContents(aFile, aExpectedContents) {
107 let deferred = Promise.defer();
108 NetUtil.asyncFetch(aFile, function(aInputStream, aStatus) {
109 do_check_true(Components.isSuccessCode(aStatus));
110 let contents = NetUtil.readInputStreamToString(aInputStream,
111 aInputStream.available());
112 if (contents.length <= TEST_DATA_SHORT.length * 2) {
113 do_check_eq(contents, aExpectedContents);
115 // Do not print the entire content string to the test log.
116 do_check_eq(contents.length, aExpectedContents.length);
117 do_check_true(contents == aExpectedContents);
121 return deferred.promise;
125 * Waits for the given saver object to complete.
128 * The saver, with the output stream or a stream listener implementation.
129 * @param aOnTargetChangeFn
130 * Optional callback invoked with the target file name when it changes.
133 * @resolves When onSaveComplete is called with a success code.
134 * @rejects With an exception, if onSaveComplete is called with a failure code.
136 function promiseSaverComplete(aSaver, aOnTargetChangeFn) {
137 let deferred = Promise.defer();
139 onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget)
141 if (aOnTargetChangeFn) {
142 aOnTargetChangeFn(aTarget);
145 onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus)
147 if (Components.isSuccessCode(aStatus)) {
150 deferred.reject(new Components.Exception("Saver failed.", aStatus));
154 return deferred.promise;
158 * Feeds a string to a BackgroundFileSaverOutputStream.
160 * @param aSourceString
161 * The source data to copy.
162 * @param aSaverOutputStream
163 * The BackgroundFileSaverOutputStream to feed.
164 * @param aCloseWhenDone
165 * If true, the output stream will be closed when the copy finishes.
168 * @resolves When the copy completes with a success code.
169 * @rejects With an exception, if the copy fails.
171 function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) {
172 let deferred = Promise.defer();
173 let inputStream = new StringInputStream(aSourceString, aSourceString.length);
174 let copier = Cc["@mozilla.org/network/async-stream-copier;1"]
175 .createInstance(Ci.nsIAsyncStreamCopier);
176 copier.init(inputStream, aSaverOutputStream, null, false, true, 0x8000, true,
179 onStartRequest: function () { },
180 onStopRequest: function (aRequest, aContext, aStatusCode)
182 if (Components.isSuccessCode(aStatusCode)) {
185 deferred.reject(new Components.Exception(aResult));
189 return deferred.promise;
193 * Feeds a string to a BackgroundFileSaverStreamListener.
195 * @param aSourceString
196 * The source data to copy.
197 * @param aSaverStreamListener
198 * The BackgroundFileSaverStreamListener to feed.
199 * @param aCloseWhenDone
200 * If true, the output stream will be closed when the copy finishes.
203 * @resolves When the operation completes with a success code.
204 * @rejects With an exception, if the operation fails.
206 function promisePumpToSaver(aSourceString, aSaverStreamListener,
208 let deferred = Promise.defer();
209 aSaverStreamListener.QueryInterface(Ci.nsIStreamListener);
210 let inputStream = new StringInputStream(aSourceString, aSourceString.length);
211 let pump = Cc["@mozilla.org/network/input-stream-pump;1"]
212 .createInstance(Ci.nsIInputStreamPump);
213 pump.init(inputStream, -1, -1, 0, 0, true);
215 onStartRequest: function PPTS_onStartRequest(aRequest, aContext)
217 aSaverStreamListener.onStartRequest(aRequest, aContext);
219 onStopRequest: function PPTS_onStopRequest(aRequest, aContext, aStatusCode)
221 aSaverStreamListener.onStopRequest(aRequest, aContext, aStatusCode);
222 if (Components.isSuccessCode(aStatusCode)) {
225 deferred.reject(new Components.Exception(aResult));
228 onDataAvailable: function PPTS_onDataAvailable(aRequest, aContext,
229 aInputStream, aOffset,
232 aSaverStreamListener.onDataAvailable(aRequest, aContext, aInputStream,
236 return deferred.promise;
239 let gStillRunning = true;
241 ////////////////////////////////////////////////////////////////////////////////
249 add_task(function test_setup()
251 // Wait 10 minutes, that is half of the external xpcshell timeout.
252 do_timeout(10 * 60 * 1000, function() {
254 do_throw("Test timed out.");
259 add_task(function test_normal()
261 // This test demonstrates the most basic use case.
262 let destFile = getTempFile(TEST_FILE_NAME_1);
264 // Create the object implementing the output stream.
265 let saver = new BackgroundFileSaverOutputStream();
267 // Set up callbacks for completion and target file name change.
268 let receivedOnTargetChange = false;
269 function onTargetChange(aTarget) {
270 do_check_true(destFile.equals(aTarget));
271 receivedOnTargetChange = true;
273 let completionPromise = promiseSaverComplete(saver, onTargetChange);
275 // Set the target file.
276 saver.setTarget(destFile, false);
278 // Write some data and close the output stream.
279 yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
281 // Indicate that we are ready to finish, and wait for a successful callback.
282 saver.finish(Cr.NS_OK);
283 yield completionPromise;
285 // Only after we receive the completion notification, we can also be sure that
286 // we've received the target file name change notification before it.
287 do_check_true(receivedOnTargetChange);
290 destFile.remove(false);
293 add_task(function test_combinations()
295 let initialFile = getTempFile(TEST_FILE_NAME_1);
296 let renamedFile = getTempFile(TEST_FILE_NAME_2);
298 // Keep track of the current file.
299 let currentFile = null;
300 function onTargetChange(aTarget) {
302 do_print("Target file changed to: " + aTarget.leafName);
303 currentFile = aTarget;
306 // Tests various combinations of events and behaviors for both the stream
307 // listener and the output stream implementations.
308 for (let testFlags = 0; testFlags < 32; testFlags++) {
309 let keepPartialOnFailure = !!(testFlags & 1);
310 let renameAtSomePoint = !!(testFlags & 2);
311 let cancelAtSomePoint = !!(testFlags & 4);
312 let useStreamListener = !!(testFlags & 8);
313 let useLongData = !!(testFlags & 16);
315 let startTime = Date.now();
316 do_print("Starting keepPartialOnFailure = " + keepPartialOnFailure +
317 ", renameAtSomePoint = " + renameAtSomePoint +
318 ", cancelAtSomePoint = " + cancelAtSomePoint +
319 ", useStreamListener = " + useStreamListener +
320 ", useLongData = " + useLongData);
322 // Create the object and register the observers.
324 let saver = useStreamListener
325 ? new BackgroundFileSaverStreamListener()
326 : new BackgroundFileSaverOutputStream();
327 saver.enableSha256();
328 let completionPromise = promiseSaverComplete(saver, onTargetChange);
330 // Start feeding the first chunk of data to the saver. In case we are using
331 // the stream listener, we only write one chunk.
332 let testData = useLongData ? TEST_DATA_LONG : TEST_DATA_SHORT;
333 let feedPromise = useStreamListener
334 ? promisePumpToSaver(testData + testData, saver)
335 : promiseCopyToSaver(testData, saver, false);
337 // Set a target output file.
338 saver.setTarget(initialFile, keepPartialOnFailure);
340 // Wait for the first chunk of data to be copied.
343 if (renameAtSomePoint) {
344 saver.setTarget(renamedFile, keepPartialOnFailure);
347 if (cancelAtSomePoint) {
348 saver.finish(Cr.NS_ERROR_FAILURE);
351 // Feed the second chunk of data to the saver.
352 if (!useStreamListener) {
353 yield promiseCopyToSaver(testData, saver, true);
356 // Wait for completion, and ensure we succeeded or failed as expected.
357 if (!cancelAtSomePoint) {
358 saver.finish(Cr.NS_OK);
361 yield completionPromise;
362 if (cancelAtSomePoint) {
363 do_throw("Failure expected.");
365 } catch (ex if cancelAtSomePoint && ex.result == Cr.NS_ERROR_FAILURE) { }
367 if (!cancelAtSomePoint) {
368 // In this case, the file must exist.
369 do_check_true(currentFile.exists());
370 let expectedContents = testData + testData;
371 yield promiseVerifyContents(currentFile, expectedContents);
372 do_check_eq(EXPECTED_HASHES[expectedContents.length],
373 toHex(saver.sha256Hash));
374 currentFile.remove(false);
376 // If the target was really renamed, the old file should not exist.
377 if (renamedFile.equals(currentFile)) {
378 do_check_false(initialFile.exists());
380 } else if (!keepPartialOnFailure) {
381 // In this case, the file must not exist.
382 do_check_false(initialFile.exists());
383 do_check_false(renamedFile.exists());
385 // In this case, the file may or may not exist, because canceling can
386 // interrupt the asynchronous operation at any point, even before the file
387 // has been created for the first time.
388 if (initialFile.exists()) {
389 initialFile.remove(false);
391 if (renamedFile.exists()) {
392 renamedFile.remove(false);
396 do_print("Test case completed in " + (Date.now() - startTime) + " ms.");
400 add_task(function test_setTarget_after_close_stream()
402 // This test checks the case where we close the output stream before we call
403 // the setTarget method. All the data should be buffered and written anyway.
404 let destFile = getTempFile(TEST_FILE_NAME_1);
406 // Test the case where the file does not already exists first, then the case
407 // where the file already exists.
408 for (let i = 0; i < 2; i++) {
409 let saver = new BackgroundFileSaverOutputStream();
410 saver.enableSha256();
411 let completionPromise = promiseSaverComplete(saver);
413 // Copy some data to the output stream of the file saver. This data must
414 // be shorter than the internal component's pipe buffer for the test to
415 // succeed, because otherwise the test would block waiting for the write to
417 yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
419 // Set the target file and wait for the output to finish.
420 saver.setTarget(destFile, false);
421 saver.finish(Cr.NS_OK);
422 yield completionPromise;
425 yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
426 do_check_eq(EXPECTED_HASHES[TEST_DATA_SHORT.length],
427 toHex(saver.sha256Hash));
431 destFile.remove(false);
434 add_task(function test_setTarget_fast()
436 // This test checks a fast rename of the target file.
437 let destFile1 = getTempFile(TEST_FILE_NAME_1);
438 let destFile2 = getTempFile(TEST_FILE_NAME_2);
439 let saver = new BackgroundFileSaverOutputStream();
440 let completionPromise = promiseSaverComplete(saver);
442 // Set the initial name after the stream is closed, then rename immediately.
443 yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
444 saver.setTarget(destFile1, false);
445 saver.setTarget(destFile2, false);
447 // Wait for all the operations to complete.
448 saver.finish(Cr.NS_OK);
449 yield completionPromise;
451 // Verify results and clean up.
452 do_check_false(destFile1.exists());
453 yield promiseVerifyContents(destFile2, TEST_DATA_SHORT);
454 destFile2.remove(false);
457 add_task(function test_setTarget_multiple()
459 // This test checks multiple renames of the target file.
460 let destFile = getTempFile(TEST_FILE_NAME_1);
461 let saver = new BackgroundFileSaverOutputStream();
462 let completionPromise = promiseSaverComplete(saver);
464 // Rename both before and after the stream is closed.
465 saver.setTarget(getTempFile(TEST_FILE_NAME_2), false);
466 saver.setTarget(getTempFile(TEST_FILE_NAME_3), false);
467 yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
468 saver.setTarget(getTempFile(TEST_FILE_NAME_2), false);
469 saver.setTarget(destFile, false);
471 // Wait for all the operations to complete.
472 saver.finish(Cr.NS_OK);
473 yield completionPromise;
475 // Verify results and clean up.
476 do_check_false(getTempFile(TEST_FILE_NAME_2).exists());
477 do_check_false(getTempFile(TEST_FILE_NAME_3).exists());
478 yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
479 destFile.remove(false);
482 add_task(function test_enableAppend()
484 // This test checks append mode with hashing disabled.
485 let destFile = getTempFile(TEST_FILE_NAME_1);
487 // Test the case where the file does not already exists first, then the case
488 // where the file already exists.
489 for (let i = 0; i < 2; i++) {
490 let saver = new BackgroundFileSaverOutputStream();
491 saver.enableAppend();
492 let completionPromise = promiseSaverComplete(saver);
494 saver.setTarget(destFile, false);
495 yield promiseCopyToSaver(TEST_DATA_LONG, saver, true);
497 saver.finish(Cr.NS_OK);
498 yield completionPromise;
501 let expectedContents = (i == 0 ? TEST_DATA_LONG
502 : TEST_DATA_LONG + TEST_DATA_LONG);
503 yield promiseVerifyContents(destFile, expectedContents);
507 destFile.remove(false);
510 add_task(function test_enableAppend_setTarget_fast()
512 // This test checks a fast rename of the target file in append mode.
513 let destFile1 = getTempFile(TEST_FILE_NAME_1);
514 let destFile2 = getTempFile(TEST_FILE_NAME_2);
516 // Test the case where the file does not already exists first, then the case
517 // where the file already exists.
518 for (let i = 0; i < 2; i++) {
519 let saver = new BackgroundFileSaverOutputStream();
520 saver.enableAppend();
521 let completionPromise = promiseSaverComplete(saver);
523 yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
525 // The first time, we start appending to the first file and rename to the
526 // second file. The second time, we start appending to the second file,
527 // that was created the first time, and rename back to the first file.
528 let firstFile = (i == 0) ? destFile1 : destFile2;
529 let secondFile = (i == 0) ? destFile2 : destFile1;
530 saver.setTarget(firstFile, false);
531 saver.setTarget(secondFile, false);
533 saver.finish(Cr.NS_OK);
534 yield completionPromise;
537 do_check_false(firstFile.exists());
538 let expectedContents = (i == 0 ? TEST_DATA_SHORT
539 : TEST_DATA_SHORT + TEST_DATA_SHORT);
540 yield promiseVerifyContents(secondFile, expectedContents);
544 destFile1.remove(false);
547 add_task(function test_enableAppend_hash()
549 // This test checks append mode, also verifying that the computed hash
550 // includes the contents of the existing data.
551 let destFile = getTempFile(TEST_FILE_NAME_1);
553 // Test the case where the file does not already exists first, then the case
554 // where the file already exists.
555 for (let i = 0; i < 2; i++) {
556 let saver = new BackgroundFileSaverOutputStream();
557 saver.enableAppend();
558 saver.enableSha256();
559 let completionPromise = promiseSaverComplete(saver);
561 saver.setTarget(destFile, false);
562 yield promiseCopyToSaver(TEST_DATA_LONG, saver, true);
564 saver.finish(Cr.NS_OK);
565 yield completionPromise;
568 let expectedContents = (i == 0 ? TEST_DATA_LONG
569 : TEST_DATA_LONG + TEST_DATA_LONG);
570 yield promiseVerifyContents(destFile, expectedContents);
571 do_check_eq(EXPECTED_HASHES[expectedContents.length],
572 toHex(saver.sha256Hash));
576 destFile.remove(false);
579 add_task(function test_finish_only()
581 // This test checks creating the object and doing nothing.
582 let destFile = getTempFile(TEST_FILE_NAME_1);
583 let saver = new BackgroundFileSaverOutputStream();
584 function onTargetChange(aTarget) {
585 do_throw("Should not receive the onTargetChange notification.");
587 let completionPromise = promiseSaverComplete(saver, onTargetChange);
588 saver.finish(Cr.NS_OK);
589 yield completionPromise;
592 add_task(function test_empty()
594 // This test checks we still create an empty file when no data is fed.
595 let destFile = getTempFile(TEST_FILE_NAME_1);
597 let saver = new BackgroundFileSaverOutputStream();
598 let completionPromise = promiseSaverComplete(saver);
600 saver.setTarget(destFile, false);
601 yield promiseCopyToSaver("", saver, true);
603 saver.finish(Cr.NS_OK);
604 yield completionPromise;
607 do_check_true(destFile.exists());
608 do_check_eq(destFile.fileSize, 0);
611 destFile.remove(false);
614 add_task(function test_empty_hash()
616 // This test checks the hash of an empty file, both in normal and append mode.
617 let destFile = getTempFile(TEST_FILE_NAME_1);
619 // Test normal mode first, then append mode.
620 for (let i = 0; i < 2; i++) {
621 let saver = new BackgroundFileSaverOutputStream();
623 saver.enableAppend();
625 saver.enableSha256();
626 let completionPromise = promiseSaverComplete(saver);
628 saver.setTarget(destFile, false);
629 yield promiseCopyToSaver("", saver, true);
631 saver.finish(Cr.NS_OK);
632 yield completionPromise;
635 do_check_eq(destFile.fileSize, 0);
636 do_check_eq(EXPECTED_HASHES[0], toHex(saver.sha256Hash));
640 destFile.remove(false);
643 add_task(function test_invalid_hash()
645 let saver = new BackgroundFileSaverStreamListener();
646 let completionPromise = promiseSaverComplete(saver);
647 // We shouldn't be able to get the hash if hashing hasn't been enabled
649 let hash = saver.sha256Hash;
650 do_throw("Shouldn't be able to get hash if hashing not enabled");
651 } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
652 // Enable hashing, but don't feed any data to saver
653 saver.enableSha256();
654 let destFile = getTempFile(TEST_FILE_NAME_1);
655 saver.setTarget(destFile, false);
656 // We don't wait on promiseSaverComplete, so the hash getter can run before
657 // or after onSaveComplete is called. However, the expected behavior is the
658 // same in both cases since the hash is only valid when the save completes
660 saver.finish(Cr.NS_ERROR_FAILURE);
662 let hash = saver.sha256Hash;
663 do_throw("Shouldn't be able to get hash if save did not succeed");
664 } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
665 // Wait for completion so that the worker thread finishes dealing with the
666 // target file. We expect it to fail.
668 yield completionPromise;
669 do_throw("completionPromise should throw");
670 } catch (ex if ex.result == Cr.NS_ERROR_FAILURE) { }
673 add_task(function test_signature()
675 // Check that we get a signature if the saver is finished.
676 let destFile = getTempFile(TEST_FILE_NAME_1);
678 let saver = new BackgroundFileSaverOutputStream();
679 let completionPromise = promiseSaverComplete(saver);
682 let signatureInfo = saver.signatureInfo;
683 do_throw("Can't get signature if saver is not complete");
684 } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
686 saver.enableSignatureInfo();
687 saver.setTarget(destFile, false);
688 yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
690 saver.finish(Cr.NS_OK);
691 yield completionPromise;
692 yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
694 // signatureInfo is an empty nsIArray
695 do_check_eq(0, saver.signatureInfo.length);
698 destFile.remove(false);
701 add_task(function test_signature_not_enabled()
703 // Check that we get a signature if the saver is finished on Windows.
704 let destFile = getTempFile(TEST_FILE_NAME_1);
706 let saver = new BackgroundFileSaverOutputStream();
707 let completionPromise = promiseSaverComplete(saver);
708 saver.setTarget(destFile, false);
709 yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
711 saver.finish(Cr.NS_OK);
712 yield completionPromise;
714 let signatureInfo = saver.signatureInfo;
715 do_throw("Can't get signature if not enabled");
716 } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
719 destFile.remove(false);
722 add_task(function test_teardown()
724 gStillRunning = false;