mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
505 lines
17 KiB
JavaScript
505 lines
17 KiB
JavaScript
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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.
|
|
*/
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
const Cr = Components.results;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//// 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/commonjs/sdk/core/promise.js");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|
"resource://gre/modules/Task.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 hash
|
|
const EXPECTED_HASHES = {
|
|
// SHA-256 hash of TEST_DATA_SHORT
|
|
40 : "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192",
|
|
// SHA-256 hash of TEST_DATA_SHORT + TEST_DATA_SHORT
|
|
80 : "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58",
|
|
// SHA-256 hash of a bunch of dashes
|
|
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(aFile, 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;
|
|
}
|
|
|
|
let 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);
|
|
|
|
// 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);
|
|
|
|
// Keep track of the current file.
|
|
let currentFile = null;
|
|
function onTargetChange(aTarget) {
|
|
do_print("Target file changed to: " + aTarget.leafName);
|
|
currentFile = aTarget;
|
|
}
|
|
|
|
// Create the object and register the observers.
|
|
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());
|
|
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_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_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_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_teardown()
|
|
{
|
|
gStillRunning = false;
|
|
});
|