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 ////////////////////////////////////////////////////////////////////////////////
15 ChromeUtils
.defineESModuleGetters(this, {
16 FileTestUtils
: "resource://testing-common/FileTestUtils.sys.mjs",
19 const BackgroundFileSaverOutputStream
= Components
.Constructor(
20 "@mozilla.org/network/background-file-saver;1?mode=outputstream",
21 "nsIBackgroundFileSaver"
24 const BackgroundFileSaverStreamListener
= Components
.Constructor(
25 "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
26 "nsIBackgroundFileSaver"
29 const StringInputStream
= Components
.Constructor(
30 "@mozilla.org/io/string-input-stream;1",
31 "nsIStringInputStream",
35 const REQUEST_SUSPEND_AT
= 1024 * 1024 * 4;
36 const TEST_DATA_SHORT
= "This test string is written to the file.";
37 const TEST_FILE_NAME_1
= "test-backgroundfilesaver-1.txt";
38 const TEST_FILE_NAME_2
= "test-backgroundfilesaver-2.txt";
39 const TEST_FILE_NAME_3
= "test-backgroundfilesaver-3.txt";
41 // A map of test data length to the expected SHA-256 hashes
42 const EXPECTED_HASHES
= {
44 0: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
46 40: "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192",
47 // TEST_DATA_SHORT + TEST_DATA_SHORT
48 80: "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58",
50 4718592: "372cb9e5ce7b76d3e2a5042e78aa72dcf973e659a262c61b7ff51df74b36767b",
51 // TEST_DATA_LONG + TEST_DATA_LONG
52 9437184: "693e4f8c6855a6fed4f5f9370d12cc53105672f3ff69783581e7d925984c41d3",
55 // Generate a long string of data in a moderately fast way.
56 const TEST_256_CHARS
= new Array(257).join("-");
57 const DESIRED_LENGTH
= REQUEST_SUSPEND_AT
* 1.125;
58 const TEST_DATA_LONG
= new Array(1 + DESIRED_LENGTH
/ 256).join(TEST_256_CHARS
);
59 Assert
.equal(TEST_DATA_LONG
.length
, DESIRED_LENGTH
);
62 * Returns a reference to a temporary file that is guaranteed not to exist and
63 * is cleaned up later. See FileTestUtils.getTempFile for details.
65 function getTempFile(leafName
) {
66 return FileTestUtils
.getTempFile(leafName
);
70 * Helper function for converting a binary blob to its hex equivalent.
73 * String possibly containing non-printable chars.
74 * @return A hex-encoded string.
78 for (var i
= 0; i
< str
.length
; i
++) {
79 hex
+= ("0" + str
.charCodeAt(i
).toString(16)).slice(-2);
85 * Ensures that the given file contents are equal to the given string.
88 * nsIFile whose contents should be verified.
89 * @param aExpectedContents
90 * String containing the octets that are expected in the file.
93 * @resolves When the operation completes.
96 function promiseVerifyContents(aFile
, aExpectedContents
) {
97 return new Promise(resolve
=> {
100 uri
: NetUtil
.newURI(aFile
),
101 loadUsingSystemPrincipal
: true,
103 function (aInputStream
, aStatus
) {
104 Assert
.ok(Components
.isSuccessCode(aStatus
));
105 let contents
= NetUtil
.readInputStreamToString(
107 aInputStream
.available()
109 if (contents
.length
<= TEST_DATA_SHORT
.length
* 2) {
110 Assert
.equal(contents
, aExpectedContents
);
112 // Do not print the entire content string to the test log.
113 Assert
.equal(contents
.length
, aExpectedContents
.length
);
114 Assert
.ok(contents
== aExpectedContents
);
123 * Waits for the given saver object to complete.
126 * The saver, with the output stream or a stream listener implementation.
127 * @param aOnTargetChangeFn
128 * Optional callback invoked with the target file name when it changes.
131 * @resolves When onSaveComplete is called with a success code.
132 * @rejects With an exception, if onSaveComplete is called with a failure code.
134 function promiseSaverComplete(aSaver
, aOnTargetChangeFn
) {
135 return new Promise((resolve
, reject
) => {
137 onTargetChange
: function BFSO_onSaveComplete(saver
, aTarget
) {
138 if (aOnTargetChangeFn
) {
139 aOnTargetChangeFn(aTarget
);
142 onSaveComplete
: function BFSO_onSaveComplete(saver
, aStatus
) {
143 if (Components
.isSuccessCode(aStatus
)) {
146 reject(new Components
.Exception("Saver failed.", aStatus
));
154 * Feeds a string to a BackgroundFileSaverOutputStream.
156 * @param aSourceString
157 * The source data to copy.
158 * @param aSaverOutputStream
159 * The BackgroundFileSaverOutputStream to feed.
160 * @param aCloseWhenDone
161 * If true, the output stream will be closed when the copy finishes.
164 * @resolves When the copy completes with a success code.
165 * @rejects With an exception, if the copy fails.
167 function promiseCopyToSaver(aSourceString
, aSaverOutputStream
, aCloseWhenDone
) {
168 return new Promise((resolve
, reject
) => {
169 let inputStream
= new StringInputStream(aSourceString
);
171 "@mozilla.org/network/async-stream-copier;1"
172 ].createInstance(Ci
.nsIAsyncStreamCopier
);
186 onStopRequest(aRequest
, aStatusCode
) {
187 if (Components
.isSuccessCode(aStatusCode
)) {
190 reject(new Components
.Exception(aStatusCode
));
200 * Feeds a string to a BackgroundFileSaverStreamListener.
202 * @param aSourceString
203 * The source data to copy.
204 * @param aSaverStreamListener
205 * The BackgroundFileSaverStreamListener to feed.
206 * @param aCloseWhenDone
207 * If true, the output stream will be closed when the copy finishes.
210 * @resolves When the operation completes with a success code.
211 * @rejects With an exception, if the operation fails.
213 function promisePumpToSaver(aSourceString
, aSaverStreamListener
) {
214 return new Promise((resolve
, reject
) => {
215 aSaverStreamListener
.QueryInterface(Ci
.nsIStreamListener
);
216 let inputStream
= new StringInputStream(aSourceString
);
217 let pump
= Cc
["@mozilla.org/network/input-stream-pump;1"].createInstance(
218 Ci
.nsIInputStreamPump
220 pump
.init(inputStream
, 0, 0, true);
222 onStartRequest
: function PPTS_onStartRequest(aRequest
) {
223 aSaverStreamListener
.onStartRequest(aRequest
);
225 onStopRequest
: function PPTS_onStopRequest(aRequest
, aStatusCode
) {
226 aSaverStreamListener
.onStopRequest(aRequest
, aStatusCode
);
227 if (Components
.isSuccessCode(aStatusCode
)) {
230 reject(new Components
.Exception(aStatusCode
));
233 onDataAvailable
: function PPTS_onDataAvailable(
239 aSaverStreamListener
.onDataAvailable(
250 var gStillRunning
= true;
252 ////////////////////////////////////////////////////////////////////////////////
255 add_task(function test_setup() {
256 // Wait 10 minutes, that is half of the external xpcshell timeout.
257 do_timeout(10 * 60 * 1000, function () {
259 do_throw("Test timed out.");
264 add_task(async
function test_normal() {
265 // This test demonstrates the most basic use case.
266 let destFile
= getTempFile(TEST_FILE_NAME_1
);
268 // Create the object implementing the output stream.
269 let saver
= new BackgroundFileSaverOutputStream();
271 // Set up callbacks for completion and target file name change.
272 let receivedOnTargetChange
= false;
273 function onTargetChange(aTarget
) {
274 Assert
.ok(destFile
.equals(aTarget
));
275 receivedOnTargetChange
= true;
277 let completionPromise
= promiseSaverComplete(saver
, onTargetChange
);
279 // Set the target file.
280 saver
.setTarget(destFile
, false);
282 // Write some data and close the output stream.
283 await
promiseCopyToSaver(TEST_DATA_SHORT
, saver
, true);
285 // Indicate that we are ready to finish, and wait for a successful callback.
286 saver
.finish(Cr
.NS_OK
);
287 await completionPromise
;
289 // Only after we receive the completion notification, we can also be sure that
290 // we've received the target file name change notification before it.
291 Assert
.ok(receivedOnTargetChange
);
294 destFile
.remove(false);
297 add_task(async
function test_combinations() {
298 let initialFile
= getTempFile(TEST_FILE_NAME_1
);
299 let renamedFile
= getTempFile(TEST_FILE_NAME_2
);
301 // Keep track of the current file.
302 let currentFile
= null;
303 function onTargetChange(aTarget
) {
305 info("Target file changed to: " + aTarget
.leafName
);
306 currentFile
= aTarget
;
309 // Tests various combinations of events and behaviors for both the stream
310 // listener and the output stream implementations.
311 for (let testFlags
= 0; testFlags
< 32; testFlags
++) {
312 let keepPartialOnFailure
= !!(testFlags
& 1);
313 let renameAtSomePoint
= !!(testFlags
& 2);
314 let cancelAtSomePoint
= !!(testFlags
& 4);
315 let useStreamListener
= !!(testFlags
& 8);
316 let useLongData
= !!(testFlags
& 16);
318 let startTime
= Date
.now();
320 "Starting keepPartialOnFailure = " +
321 keepPartialOnFailure
+
322 ", renameAtSomePoint = " +
324 ", cancelAtSomePoint = " +
326 ", useStreamListener = " +
332 // Create the object and register the observers.
334 let saver
= useStreamListener
335 ? new BackgroundFileSaverStreamListener()
336 : new BackgroundFileSaverOutputStream();
337 saver
.enableSha256();
338 let completionPromise
= promiseSaverComplete(saver
, onTargetChange
);
340 // Start feeding the first chunk of data to the saver. In case we are using
341 // the stream listener, we only write one chunk.
342 let testData
= useLongData
? TEST_DATA_LONG
: TEST_DATA_SHORT
;
343 let feedPromise
= useStreamListener
344 ? promisePumpToSaver(testData
+ testData
, saver
)
345 : promiseCopyToSaver(testData
, saver
, false);
347 // Set a target output file.
348 saver
.setTarget(initialFile
, keepPartialOnFailure
);
350 // Wait for the first chunk of data to be copied.
353 if (renameAtSomePoint
) {
354 saver
.setTarget(renamedFile
, keepPartialOnFailure
);
357 if (cancelAtSomePoint
) {
358 saver
.finish(Cr
.NS_ERROR_FAILURE
);
361 // Feed the second chunk of data to the saver.
362 if (!useStreamListener
) {
363 await
promiseCopyToSaver(testData
, saver
, true);
366 // Wait for completion, and ensure we succeeded or failed as expected.
367 if (!cancelAtSomePoint
) {
368 saver
.finish(Cr
.NS_OK
);
371 await completionPromise
;
372 if (cancelAtSomePoint
) {
373 do_throw("Failure expected.");
376 if (!cancelAtSomePoint
|| ex
.result
!= Cr
.NS_ERROR_FAILURE
) {
381 if (!cancelAtSomePoint
) {
382 // In this case, the file must exist.
383 Assert
.ok(currentFile
.exists());
384 let expectedContents
= testData
+ testData
;
385 await
promiseVerifyContents(currentFile
, expectedContents
);
387 EXPECTED_HASHES
[expectedContents
.length
],
388 toHex(saver
.sha256Hash
)
390 currentFile
.remove(false);
392 // If the target was really renamed, the old file should not exist.
393 if (renamedFile
.equals(currentFile
)) {
394 Assert
.ok(!initialFile
.exists());
396 } else if (!keepPartialOnFailure
) {
397 // In this case, the file must not exist.
398 Assert
.ok(!initialFile
.exists());
399 Assert
.ok(!renamedFile
.exists());
401 // In this case, the file may or may not exist, because canceling can
402 // interrupt the asynchronous operation at any point, even before the file
403 // has been created for the first time.
404 if (initialFile
.exists()) {
405 initialFile
.remove(false);
407 if (renamedFile
.exists()) {
408 renamedFile
.remove(false);
412 info("Test case completed in " + (Date
.now() - startTime
) + " ms.");
416 add_task(async
function test_setTarget_after_close_stream() {
417 // This test checks the case where we close the output stream before we call
418 // the setTarget method. All the data should be buffered and written anyway.
419 let destFile
= getTempFile(TEST_FILE_NAME_1
);
421 // Test the case where the file does not already exists first, then the case
422 // where the file already exists.
423 for (let i
= 0; i
< 2; i
++) {
424 let saver
= new BackgroundFileSaverOutputStream();
425 saver
.enableSha256();
426 let completionPromise
= promiseSaverComplete(saver
);
428 // Copy some data to the output stream of the file saver. This data must
429 // be shorter than the internal component's pipe buffer for the test to
430 // succeed, because otherwise the test would block waiting for the write to
432 await
promiseCopyToSaver(TEST_DATA_SHORT
, saver
, true);
434 // Set the target file and wait for the output to finish.
435 saver
.setTarget(destFile
, false);
436 saver
.finish(Cr
.NS_OK
);
437 await completionPromise
;
440 await
promiseVerifyContents(destFile
, TEST_DATA_SHORT
);
442 EXPECTED_HASHES
[TEST_DATA_SHORT
.length
],
443 toHex(saver
.sha256Hash
)
448 destFile
.remove(false);
451 add_task(async
function test_setTarget_fast() {
452 // This test checks a fast rename of the target file.
453 let destFile1
= getTempFile(TEST_FILE_NAME_1
);
454 let destFile2
= getTempFile(TEST_FILE_NAME_2
);
455 let saver
= new BackgroundFileSaverOutputStream();
456 let completionPromise
= promiseSaverComplete(saver
);
458 // Set the initial name after the stream is closed, then rename immediately.
459 await
promiseCopyToSaver(TEST_DATA_SHORT
, saver
, true);
460 saver
.setTarget(destFile1
, false);
461 saver
.setTarget(destFile2
, false);
463 // Wait for all the operations to complete.
464 saver
.finish(Cr
.NS_OK
);
465 await completionPromise
;
467 // Verify results and clean up.
468 Assert
.ok(!destFile1
.exists());
469 await
promiseVerifyContents(destFile2
, TEST_DATA_SHORT
);
470 destFile2
.remove(false);
473 add_task(async
function test_setTarget_multiple() {
474 // This test checks multiple renames of the target file.
475 let destFile
= getTempFile(TEST_FILE_NAME_1
);
476 let saver
= new BackgroundFileSaverOutputStream();
477 let completionPromise
= promiseSaverComplete(saver
);
479 // Rename both before and after the stream is closed.
480 saver
.setTarget(getTempFile(TEST_FILE_NAME_2
), false);
481 saver
.setTarget(getTempFile(TEST_FILE_NAME_3
), false);
482 await
promiseCopyToSaver(TEST_DATA_SHORT
, saver
, true);
483 saver
.setTarget(getTempFile(TEST_FILE_NAME_2
), false);
484 saver
.setTarget(destFile
, false);
486 // Wait for all the operations to complete.
487 saver
.finish(Cr
.NS_OK
);
488 await completionPromise
;
490 // Verify results and clean up.
491 Assert
.ok(!getTempFile(TEST_FILE_NAME_2
).exists());
492 Assert
.ok(!getTempFile(TEST_FILE_NAME_3
).exists());
493 await
promiseVerifyContents(destFile
, TEST_DATA_SHORT
);
494 destFile
.remove(false);
497 add_task(async
function test_enableAppend() {
498 // This test checks append mode with hashing disabled.
499 let destFile
= getTempFile(TEST_FILE_NAME_1
);
501 // Test the case where the file does not already exists first, then the case
502 // where the file already exists.
503 for (let i
= 0; i
< 2; i
++) {
504 let saver
= new BackgroundFileSaverOutputStream();
505 saver
.enableAppend();
506 let completionPromise
= promiseSaverComplete(saver
);
508 saver
.setTarget(destFile
, false);
509 await
promiseCopyToSaver(TEST_DATA_LONG
, saver
, true);
511 saver
.finish(Cr
.NS_OK
);
512 await completionPromise
;
515 let expectedContents
=
516 i
== 0 ? TEST_DATA_LONG
: TEST_DATA_LONG
+ TEST_DATA_LONG
;
517 await
promiseVerifyContents(destFile
, expectedContents
);
521 destFile
.remove(false);
524 add_task(async
function test_enableAppend_setTarget_fast() {
525 // This test checks a fast rename of the target file in append mode.
526 let destFile1
= getTempFile(TEST_FILE_NAME_1
);
527 let destFile2
= getTempFile(TEST_FILE_NAME_2
);
529 // Test the case where the file does not already exists first, then the case
530 // where the file already exists.
531 for (let i
= 0; i
< 2; i
++) {
532 let saver
= new BackgroundFileSaverOutputStream();
533 saver
.enableAppend();
534 let completionPromise
= promiseSaverComplete(saver
);
536 await
promiseCopyToSaver(TEST_DATA_SHORT
, saver
, true);
538 // The first time, we start appending to the first file and rename to the
539 // second file. The second time, we start appending to the second file,
540 // that was created the first time, and rename back to the first file.
541 let firstFile
= i
== 0 ? destFile1
: destFile2
;
542 let secondFile
= i
== 0 ? destFile2
: destFile1
;
543 saver
.setTarget(firstFile
, false);
544 saver
.setTarget(secondFile
, false);
546 saver
.finish(Cr
.NS_OK
);
547 await completionPromise
;
550 Assert
.ok(!firstFile
.exists());
551 let expectedContents
=
552 i
== 0 ? TEST_DATA_SHORT
: TEST_DATA_SHORT
+ TEST_DATA_SHORT
;
553 await
promiseVerifyContents(secondFile
, expectedContents
);
557 destFile1
.remove(false);
560 add_task(async
function test_enableAppend_hash() {
561 // This test checks append mode, also verifying that the computed hash
562 // includes the contents of the existing data.
563 let destFile
= getTempFile(TEST_FILE_NAME_1
);
565 // Test the case where the file does not already exists first, then the case
566 // where the file already exists.
567 for (let i
= 0; i
< 2; i
++) {
568 let saver
= new BackgroundFileSaverOutputStream();
569 saver
.enableAppend();
570 saver
.enableSha256();
571 let completionPromise
= promiseSaverComplete(saver
);
573 saver
.setTarget(destFile
, false);
574 await
promiseCopyToSaver(TEST_DATA_LONG
, saver
, true);
576 saver
.finish(Cr
.NS_OK
);
577 await completionPromise
;
580 let expectedContents
=
581 i
== 0 ? TEST_DATA_LONG
: TEST_DATA_LONG
+ TEST_DATA_LONG
;
582 await
promiseVerifyContents(destFile
, expectedContents
);
584 EXPECTED_HASHES
[expectedContents
.length
],
585 toHex(saver
.sha256Hash
)
590 destFile
.remove(false);
593 add_task(async
function test_finish_only() {
594 // This test checks creating the object and doing nothing.
595 let saver
= new BackgroundFileSaverOutputStream();
596 function onTargetChange() {
597 do_throw("Should not receive the onTargetChange notification.");
599 let completionPromise
= promiseSaverComplete(saver
, onTargetChange
);
600 saver
.finish(Cr
.NS_OK
);
601 await completionPromise
;
604 add_task(async
function test_empty() {
605 // This test checks we still create an empty file when no data is fed.
606 let destFile
= getTempFile(TEST_FILE_NAME_1
);
608 let saver
= new BackgroundFileSaverOutputStream();
609 let completionPromise
= promiseSaverComplete(saver
);
611 saver
.setTarget(destFile
, false);
612 await
promiseCopyToSaver("", saver
, true);
614 saver
.finish(Cr
.NS_OK
);
615 await completionPromise
;
618 Assert
.ok(destFile
.exists());
619 Assert
.equal(destFile
.fileSize
, 0);
622 destFile
.remove(false);
625 add_task(async
function test_empty_hash() {
626 // This test checks the hash of an empty file, both in normal and append mode.
627 let destFile
= getTempFile(TEST_FILE_NAME_1
);
629 // Test normal mode first, then append mode.
630 for (let i
= 0; i
< 2; i
++) {
631 let saver
= new BackgroundFileSaverOutputStream();
633 saver
.enableAppend();
635 saver
.enableSha256();
636 let completionPromise
= promiseSaverComplete(saver
);
638 saver
.setTarget(destFile
, false);
639 await
promiseCopyToSaver("", saver
, true);
641 saver
.finish(Cr
.NS_OK
);
642 await completionPromise
;
645 Assert
.equal(destFile
.fileSize
, 0);
646 Assert
.equal(EXPECTED_HASHES
[0], toHex(saver
.sha256Hash
));
650 destFile
.remove(false);
653 add_task(async
function test_invalid_hash() {
654 let saver
= new BackgroundFileSaverStreamListener();
655 let completionPromise
= promiseSaverComplete(saver
);
656 // We shouldn't be able to get the hash if hashing hasn't been enabled
659 do_throw("Shouldn't be able to get hash if hashing not enabled");
661 if (ex
.result
!= Cr
.NS_ERROR_NOT_AVAILABLE
) {
665 // Enable hashing, but don't feed any data to saver
666 saver
.enableSha256();
667 let destFile
= getTempFile(TEST_FILE_NAME_1
);
668 saver
.setTarget(destFile
, false);
669 // We don't wait on promiseSaverComplete, so the hash getter can run before
670 // or after onSaveComplete is called. However, the expected behavior is the
671 // same in both cases since the hash is only valid when the save completes
673 saver
.finish(Cr
.NS_ERROR_FAILURE
);
676 do_throw("Shouldn't be able to get hash if save did not succeed");
678 if (ex
.result
!= Cr
.NS_ERROR_NOT_AVAILABLE
) {
682 // Wait for completion so that the worker thread finishes dealing with the
683 // target file. We expect it to fail.
685 await completionPromise
;
686 do_throw("completionPromise should throw");
688 if (ex
.result
!= Cr
.NS_ERROR_FAILURE
) {
694 add_task(async
function test_signature() {
695 // Check that we get a signature if the saver is finished.
696 let destFile
= getTempFile(TEST_FILE_NAME_1
);
698 let saver
= new BackgroundFileSaverOutputStream();
699 let completionPromise
= promiseSaverComplete(saver
);
703 do_throw("Can't get signature if saver is not complete");
705 if (ex
.result
!= Cr
.NS_ERROR_NOT_AVAILABLE
) {
710 saver
.enableSignatureInfo();
711 saver
.setTarget(destFile
, false);
712 await
promiseCopyToSaver(TEST_DATA_SHORT
, saver
, true);
714 saver
.finish(Cr
.NS_OK
);
715 await completionPromise
;
716 await
promiseVerifyContents(destFile
, TEST_DATA_SHORT
);
718 // signatureInfo is an empty nsIArray
719 Assert
.equal(0, saver
.signatureInfo
.length
);
722 destFile
.remove(false);
725 add_task(async
function test_signature_not_enabled() {
726 // Check that we get a signature if the saver is finished on Windows.
727 let destFile
= getTempFile(TEST_FILE_NAME_1
);
729 let saver
= new BackgroundFileSaverOutputStream();
730 let completionPromise
= promiseSaverComplete(saver
);
731 saver
.setTarget(destFile
, false);
732 await
promiseCopyToSaver(TEST_DATA_SHORT
, saver
, true);
734 saver
.finish(Cr
.NS_OK
);
735 await completionPromise
;
738 do_throw("Can't get signature if not enabled");
740 if (ex
.result
!= Cr
.NS_ERROR_NOT_AVAILABLE
) {
746 destFile
.remove(false);
749 add_task(function test_teardown() {
750 gStillRunning
= false;