gecko-dev/netwerk/test/unit/test_backgroundfilesaver.js

732 lines
24 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This file tests components that implement nsIBackgroundFileSaver.
*/
////////////////////////////////////////////////////////////////////////////////
//// Globals
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
const BackgroundFileSaverOutputStream = Components.Constructor(
"@mozilla.org/network/background-file-saver;1?mode=outputstream",
"nsIBackgroundFileSaver");
const BackgroundFileSaverStreamListener = Components.Constructor(
"@mozilla.org/network/background-file-saver;1?mode=streamlistener",
"nsIBackgroundFileSaver");
const StringInputStream = Components.Constructor(
"@mozilla.org/io/string-input-stream;1",
"nsIStringInputStream",
"setData");
const REQUEST_SUSPEND_AT = 1024 * 1024 * 4;
const TEST_DATA_SHORT = "This test string is written to the file.";
const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt";
const TEST_FILE_NAME_2 = "test-backgroundfilesaver-2.txt";
const TEST_FILE_NAME_3 = "test-backgroundfilesaver-3.txt";
// A map of test data length to the expected SHA-256 hashes
const EXPECTED_HASHES = {
// No data
0 : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
// TEST_DATA_SHORT
40 : "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192",
// TEST_DATA_SHORT + TEST_DATA_SHORT
80 : "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58",
// TEST_DATA_LONG
8388608 : "e3611a47714c42bdf326acfb2eb6ed9fa4cca65cb7d7be55217770a5bf5e7ff0",
// TEST_DATA_LONG + TEST_DATA_LONG
16777216 : "03a0db69a30140f307587ee746a539247c181bafd85b85c8516a3533c7d9ea1d"
};
const gTextDecoder = new TextDecoder();
// Generate a long string of data in a moderately fast way.
const TEST_256_CHARS = new Array(257).join("-");
const DESIRED_LENGTH = REQUEST_SUSPEND_AT * 2;
const TEST_DATA_LONG = new Array(1 + DESIRED_LENGTH / 256).join(TEST_256_CHARS);
do_check_eq(TEST_DATA_LONG.length, DESIRED_LENGTH);
/**
* Returns a reference to a temporary file. If the file is then created, it
* will be removed when tests in this file finish.
*/
function getTempFile(aLeafName) {
let file = FileUtils.getFile("TmpD", [aLeafName]);
do_register_cleanup(function GTF_cleanup() {
if (file.exists()) {
file.remove(false);
}
});
return file;
}
/**
* Helper function for converting a binary blob to its hex equivalent.
*
* @param str
* String possibly containing non-printable chars.
* @return A hex-encoded string.
*/
function toHex(str) {
var hex = '';
for (var i = 0; i < str.length; i++) {
hex += ('0' + str.charCodeAt(i).toString(16)).slice(-2);
}
return hex;
}
/**
* Ensures that the given file contents are equal to the given string.
*
* @param aFile
* nsIFile whose contents should be verified.
* @param aExpectedContents
* String containing the octets that are expected in the file.
*
* @return {Promise}
* @resolves When the operation completes.
* @rejects Never.
*/
function promiseVerifyContents(aFile, aExpectedContents) {
let deferred = Promise.defer();
NetUtil.asyncFetch({
uri: NetUtil.newURI(aFile),
loadUsingSystemPrincipal: true
}, function(aInputStream, aStatus) {
do_check_true(Components.isSuccessCode(aStatus));
let contents = NetUtil.readInputStreamToString(aInputStream,
aInputStream.available());
if (contents.length <= TEST_DATA_SHORT.length * 2) {
do_check_eq(contents, aExpectedContents);
} else {
// Do not print the entire content string to the test log.
do_check_eq(contents.length, aExpectedContents.length);
do_check_true(contents == aExpectedContents);
}
deferred.resolve();
});
return deferred.promise;
}
/**
* Waits for the given saver object to complete.
*
* @param aSaver
* The saver, with the output stream or a stream listener implementation.
* @param aOnTargetChangeFn
* Optional callback invoked with the target file name when it changes.
*
* @return {Promise}
* @resolves When onSaveComplete is called with a success code.
* @rejects With an exception, if onSaveComplete is called with a failure code.
*/
function promiseSaverComplete(aSaver, aOnTargetChangeFn) {
let deferred = Promise.defer();
aSaver.observer = {
onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget)
{
if (aOnTargetChangeFn) {
aOnTargetChangeFn(aTarget);
}
},
onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus)
{
if (Components.isSuccessCode(aStatus)) {
deferred.resolve();
} else {
deferred.reject(new Components.Exception("Saver failed.", aStatus));
}
},
};
return deferred.promise;
}
/**
* Feeds a string to a BackgroundFileSaverOutputStream.
*
* @param aSourceString
* The source data to copy.
* @param aSaverOutputStream
* The BackgroundFileSaverOutputStream to feed.
* @param aCloseWhenDone
* If true, the output stream will be closed when the copy finishes.
*
* @return {Promise}
* @resolves When the copy completes with a success code.
* @rejects With an exception, if the copy fails.
*/
function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) {
let deferred = Promise.defer();
let inputStream = new StringInputStream(aSourceString, aSourceString.length);
let copier = Cc["@mozilla.org/network/async-stream-copier;1"]
.createInstance(Ci.nsIAsyncStreamCopier);
copier.init(inputStream, aSaverOutputStream, null, false, true, 0x8000, true,
aCloseWhenDone);
copier.asyncCopy({
onStartRequest: function () { },
onStopRequest: function (aRequest, aContext, aStatusCode)
{
if (Components.isSuccessCode(aStatusCode)) {
deferred.resolve();
} else {
deferred.reject(new Components.Exception(aResult));
}
},
}, null);
return deferred.promise;
}
/**
* Feeds a string to a BackgroundFileSaverStreamListener.
*
* @param aSourceString
* The source data to copy.
* @param aSaverStreamListener
* The BackgroundFileSaverStreamListener to feed.
* @param aCloseWhenDone
* If true, the output stream will be closed when the copy finishes.
*
* @return {Promise}
* @resolves When the operation completes with a success code.
* @rejects With an exception, if the operation fails.
*/
function promisePumpToSaver(aSourceString, aSaverStreamListener,
aCloseWhenDone) {
let deferred = Promise.defer();
aSaverStreamListener.QueryInterface(Ci.nsIStreamListener);
let inputStream = new StringInputStream(aSourceString, aSourceString.length);
let pump = Cc["@mozilla.org/network/input-stream-pump;1"]
.createInstance(Ci.nsIInputStreamPump);
pump.init(inputStream, -1, -1, 0, 0, true);
pump.asyncRead({
onStartRequest: function PPTS_onStartRequest(aRequest, aContext)
{
aSaverStreamListener.onStartRequest(aRequest, aContext);
},
onStopRequest: function PPTS_onStopRequest(aRequest, aContext, aStatusCode)
{
aSaverStreamListener.onStopRequest(aRequest, aContext, aStatusCode);
if (Components.isSuccessCode(aStatusCode)) {
deferred.resolve();
} else {
deferred.reject(new Components.Exception(aResult));
}
},
onDataAvailable: function PPTS_onDataAvailable(aRequest, aContext,
aInputStream, aOffset,
aCount)
{
aSaverStreamListener.onDataAvailable(aRequest, aContext, aInputStream,
aOffset, aCount);
},
}, null);
return deferred.promise;
}
var gStillRunning = true;
////////////////////////////////////////////////////////////////////////////////
//// Tests
function run_test()
{
run_next_test();
}
add_task(function test_setup()
{
// Wait 10 minutes, that is half of the external xpcshell timeout.
do_timeout(10 * 60 * 1000, function() {
if (gStillRunning) {
do_throw("Test timed out.");
}
})
});
add_task(function* test_normal()
{
// This test demonstrates the most basic use case.
let destFile = getTempFile(TEST_FILE_NAME_1);
// Create the object implementing the output stream.
let saver = new BackgroundFileSaverOutputStream();
// Set up callbacks for completion and target file name change.
let receivedOnTargetChange = false;
function onTargetChange(aTarget) {
do_check_true(destFile.equals(aTarget));
receivedOnTargetChange = true;
}
let completionPromise = promiseSaverComplete(saver, onTargetChange);
// Set the target file.
saver.setTarget(destFile, false);
// Write some data and close the output stream.
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
// Indicate that we are ready to finish, and wait for a successful callback.
saver.finish(Cr.NS_OK);
yield completionPromise;
// Only after we receive the completion notification, we can also be sure that
// we've received the target file name change notification before it.
do_check_true(receivedOnTargetChange);
// Clean up.
destFile.remove(false);
});
add_task(function* test_combinations()
{
let initialFile = getTempFile(TEST_FILE_NAME_1);
let renamedFile = getTempFile(TEST_FILE_NAME_2);
// Keep track of the current file.
let currentFile = null;
function onTargetChange(aTarget) {
currentFile = null;
do_print("Target file changed to: " + aTarget.leafName);
currentFile = aTarget;
}
// Tests various combinations of events and behaviors for both the stream
// listener and the output stream implementations.
for (let testFlags = 0; testFlags < 32; testFlags++) {
let keepPartialOnFailure = !!(testFlags & 1);
let renameAtSomePoint = !!(testFlags & 2);
let cancelAtSomePoint = !!(testFlags & 4);
let useStreamListener = !!(testFlags & 8);
let useLongData = !!(testFlags & 16);
let startTime = Date.now();
do_print("Starting keepPartialOnFailure = " + keepPartialOnFailure +
", renameAtSomePoint = " + renameAtSomePoint +
", cancelAtSomePoint = " + cancelAtSomePoint +
", useStreamListener = " + useStreamListener +
", useLongData = " + useLongData);
// Create the object and register the observers.
currentFile = null;
let saver = useStreamListener
? new BackgroundFileSaverStreamListener()
: new BackgroundFileSaverOutputStream();
saver.enableSha256();
let completionPromise = promiseSaverComplete(saver, onTargetChange);
// Start feeding the first chunk of data to the saver. In case we are using
// the stream listener, we only write one chunk.
let testData = useLongData ? TEST_DATA_LONG : TEST_DATA_SHORT;
let feedPromise = useStreamListener
? promisePumpToSaver(testData + testData, saver)
: promiseCopyToSaver(testData, saver, false);
// Set a target output file.
saver.setTarget(initialFile, keepPartialOnFailure);
// Wait for the first chunk of data to be copied.
yield feedPromise;
if (renameAtSomePoint) {
saver.setTarget(renamedFile, keepPartialOnFailure);
}
if (cancelAtSomePoint) {
saver.finish(Cr.NS_ERROR_FAILURE);
}
// Feed the second chunk of data to the saver.
if (!useStreamListener) {
yield promiseCopyToSaver(testData, saver, true);
}
// Wait for completion, and ensure we succeeded or failed as expected.
if (!cancelAtSomePoint) {
saver.finish(Cr.NS_OK);
}
try {
yield completionPromise;
if (cancelAtSomePoint) {
do_throw("Failure expected.");
}
} catch (ex if cancelAtSomePoint && ex.result == Cr.NS_ERROR_FAILURE) { }
if (!cancelAtSomePoint) {
// In this case, the file must exist.
do_check_true(currentFile.exists());
let expectedContents = testData + testData;
yield promiseVerifyContents(currentFile, expectedContents);
do_check_eq(EXPECTED_HASHES[expectedContents.length],
toHex(saver.sha256Hash));
currentFile.remove(false);
// If the target was really renamed, the old file should not exist.
if (renamedFile.equals(currentFile)) {
do_check_false(initialFile.exists());
}
} else if (!keepPartialOnFailure) {
// In this case, the file must not exist.
do_check_false(initialFile.exists());
do_check_false(renamedFile.exists());
} else {
// In this case, the file may or may not exist, because canceling can
// interrupt the asynchronous operation at any point, even before the file
// has been created for the first time.
if (initialFile.exists()) {
initialFile.remove(false);
}
if (renamedFile.exists()) {
renamedFile.remove(false);
}
}
do_print("Test case completed in " + (Date.now() - startTime) + " ms.");
}
});
add_task(function* test_setTarget_after_close_stream()
{
// This test checks the case where we close the output stream before we call
// the setTarget method. All the data should be buffered and written anyway.
let destFile = getTempFile(TEST_FILE_NAME_1);
// Test the case where the file does not already exists first, then the case
// where the file already exists.
for (let i = 0; i < 2; i++) {
let saver = new BackgroundFileSaverOutputStream();
saver.enableSha256();
let completionPromise = promiseSaverComplete(saver);
// Copy some data to the output stream of the file saver. This data must
// be shorter than the internal component's pipe buffer for the test to
// succeed, because otherwise the test would block waiting for the write to
// complete.
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
// Set the target file and wait for the output to finish.
saver.setTarget(destFile, false);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
do_check_eq(EXPECTED_HASHES[TEST_DATA_SHORT.length],
toHex(saver.sha256Hash));
}
// Clean up.
destFile.remove(false);
});
add_task(function* test_setTarget_fast()
{
// This test checks a fast rename of the target file.
let destFile1 = getTempFile(TEST_FILE_NAME_1);
let destFile2 = getTempFile(TEST_FILE_NAME_2);
let saver = new BackgroundFileSaverOutputStream();
let completionPromise = promiseSaverComplete(saver);
// Set the initial name after the stream is closed, then rename immediately.
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
saver.setTarget(destFile1, false);
saver.setTarget(destFile2, false);
// Wait for all the operations to complete.
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results and clean up.
do_check_false(destFile1.exists());
yield promiseVerifyContents(destFile2, TEST_DATA_SHORT);
destFile2.remove(false);
});
add_task(function* test_setTarget_multiple()
{
// This test checks multiple renames of the target file.
let destFile = getTempFile(TEST_FILE_NAME_1);
let saver = new BackgroundFileSaverOutputStream();
let completionPromise = promiseSaverComplete(saver);
// Rename both before and after the stream is closed.
saver.setTarget(getTempFile(TEST_FILE_NAME_2), false);
saver.setTarget(getTempFile(TEST_FILE_NAME_3), false);
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
saver.setTarget(getTempFile(TEST_FILE_NAME_2), false);
saver.setTarget(destFile, false);
// Wait for all the operations to complete.
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results and clean up.
do_check_false(getTempFile(TEST_FILE_NAME_2).exists());
do_check_false(getTempFile(TEST_FILE_NAME_3).exists());
yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
destFile.remove(false);
});
add_task(function* test_enableAppend()
{
// This test checks append mode with hashing disabled.
let destFile = getTempFile(TEST_FILE_NAME_1);
// Test the case where the file does not already exists first, then the case
// where the file already exists.
for (let i = 0; i < 2; i++) {
let saver = new BackgroundFileSaverOutputStream();
saver.enableAppend();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver(TEST_DATA_LONG, saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
let expectedContents = (i == 0 ? TEST_DATA_LONG
: TEST_DATA_LONG + TEST_DATA_LONG);
yield promiseVerifyContents(destFile, expectedContents);
}
// Clean up.
destFile.remove(false);
});
add_task(function* test_enableAppend_setTarget_fast()
{
// This test checks a fast rename of the target file in append mode.
let destFile1 = getTempFile(TEST_FILE_NAME_1);
let destFile2 = getTempFile(TEST_FILE_NAME_2);
// Test the case where the file does not already exists first, then the case
// where the file already exists.
for (let i = 0; i < 2; i++) {
let saver = new BackgroundFileSaverOutputStream();
saver.enableAppend();
let completionPromise = promiseSaverComplete(saver);
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
// The first time, we start appending to the first file and rename to the
// second file. The second time, we start appending to the second file,
// that was created the first time, and rename back to the first file.
let firstFile = (i == 0) ? destFile1 : destFile2;
let secondFile = (i == 0) ? destFile2 : destFile1;
saver.setTarget(firstFile, false);
saver.setTarget(secondFile, false);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
do_check_false(firstFile.exists());
let expectedContents = (i == 0 ? TEST_DATA_SHORT
: TEST_DATA_SHORT + TEST_DATA_SHORT);
yield promiseVerifyContents(secondFile, expectedContents);
}
// Clean up.
destFile1.remove(false);
});
add_task(function* test_enableAppend_hash()
{
// This test checks append mode, also verifying that the computed hash
// includes the contents of the existing data.
let destFile = getTempFile(TEST_FILE_NAME_1);
// Test the case where the file does not already exists first, then the case
// where the file already exists.
for (let i = 0; i < 2; i++) {
let saver = new BackgroundFileSaverOutputStream();
saver.enableAppend();
saver.enableSha256();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver(TEST_DATA_LONG, saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
let expectedContents = (i == 0 ? TEST_DATA_LONG
: TEST_DATA_LONG + TEST_DATA_LONG);
yield promiseVerifyContents(destFile, expectedContents);
do_check_eq(EXPECTED_HASHES[expectedContents.length],
toHex(saver.sha256Hash));
}
// Clean up.
destFile.remove(false);
});
add_task(function* test_finish_only()
{
// This test checks creating the object and doing nothing.
let destFile = getTempFile(TEST_FILE_NAME_1);
let saver = new BackgroundFileSaverOutputStream();
function onTargetChange(aTarget) {
do_throw("Should not receive the onTargetChange notification.");
}
let completionPromise = promiseSaverComplete(saver, onTargetChange);
saver.finish(Cr.NS_OK);
yield completionPromise;
});
add_task(function* test_empty()
{
// This test checks we still create an empty file when no data is fed.
let destFile = getTempFile(TEST_FILE_NAME_1);
let saver = new BackgroundFileSaverOutputStream();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver("", saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
do_check_true(destFile.exists());
do_check_eq(destFile.fileSize, 0);
// Clean up.
destFile.remove(false);
});
add_task(function* test_empty_hash()
{
// This test checks the hash of an empty file, both in normal and append mode.
let destFile = getTempFile(TEST_FILE_NAME_1);
// Test normal mode first, then append mode.
for (let i = 0; i < 2; i++) {
let saver = new BackgroundFileSaverOutputStream();
if (i == 1) {
saver.enableAppend();
}
saver.enableSha256();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver("", saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
do_check_eq(destFile.fileSize, 0);
do_check_eq(EXPECTED_HASHES[0], toHex(saver.sha256Hash));
}
// Clean up.
destFile.remove(false);
});
add_task(function* test_invalid_hash()
{
let saver = new BackgroundFileSaverStreamListener();
let completionPromise = promiseSaverComplete(saver);
// We shouldn't be able to get the hash if hashing hasn't been enabled
try {
let hash = saver.sha256Hash;
do_throw("Shouldn't be able to get hash if hashing not enabled");
} catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
// Enable hashing, but don't feed any data to saver
saver.enableSha256();
let destFile = getTempFile(TEST_FILE_NAME_1);
saver.setTarget(destFile, false);
// We don't wait on promiseSaverComplete, so the hash getter can run before
// or after onSaveComplete is called. However, the expected behavior is the
// same in both cases since the hash is only valid when the save completes
// successfully.
saver.finish(Cr.NS_ERROR_FAILURE);
try {
let hash = saver.sha256Hash;
do_throw("Shouldn't be able to get hash if save did not succeed");
} catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
// Wait for completion so that the worker thread finishes dealing with the
// target file. We expect it to fail.
try {
yield completionPromise;
do_throw("completionPromise should throw");
} catch (ex if ex.result == Cr.NS_ERROR_FAILURE) { }
});
add_task(function* test_signature()
{
// Check that we get a signature if the saver is finished.
let destFile = getTempFile(TEST_FILE_NAME_1);
let saver = new BackgroundFileSaverOutputStream();
let completionPromise = promiseSaverComplete(saver);
try {
let signatureInfo = saver.signatureInfo;
do_throw("Can't get signature if saver is not complete");
} catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
saver.enableSignatureInfo();
saver.setTarget(destFile, false);
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
// signatureInfo is an empty nsIArray
do_check_eq(0, saver.signatureInfo.length);
// Clean up.
destFile.remove(false);
});
add_task(function* test_signature_not_enabled()
{
// Check that we get a signature if the saver is finished on Windows.
let destFile = getTempFile(TEST_FILE_NAME_1);
let saver = new BackgroundFileSaverOutputStream();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
try {
let signatureInfo = saver.signatureInfo;
do_throw("Can't get signature if not enabled");
} catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { }
// Clean up.
destFile.remove(false);
});
add_task(function test_teardown()
{
gStillRunning = false;
});