diff options
Diffstat (limited to 'toolkit/components/osfile/tests/xpcshell')
37 files changed, 3299 insertions, 0 deletions
diff --git a/toolkit/components/osfile/tests/xpcshell/.eslintrc.js b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..4cb383ff7a --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + rules: { + "no-shadow": "off", + }, +}; diff --git a/toolkit/components/osfile/tests/xpcshell/head.js b/toolkit/components/osfile/tests/xpcshell/head.js new file mode 100644 index 0000000000..8d162b9767 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/head.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// Bug 1014484 can only be reproduced by loading OS.File first from the +// CommonJS loader, so we do not want OS.File to be loaded eagerly for +// all the tests in this directory. +ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); +ChromeUtils.defineESModuleGetters(this, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); +ChromeUtils.defineModuleGetter( + this, + "NetUtil", + "resource://gre/modules/NetUtil.jsm" +); + +Services.prefs.setBoolPref("toolkit.osfile.log", true); + +/** + * As add_task, but execute the test both with native operations and + * without. + */ +function add_test_pair(generator) { + add_task(async function() { + info("Executing test " + generator.name + " with native operations"); + Services.prefs.setBoolPref("toolkit.osfile.native", true); + return generator(); + }); + add_task(async function() { + info("Executing test " + generator.name + " without native operations"); + Services.prefs.setBoolPref("toolkit.osfile.native", false); + return generator(); + }); +} + +/** + * Fetch asynchronously the contents of a file using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} path The _absolute_ path to the file. + * @return {promise} + * @resolves {string} The contents of the file. + */ +function reference_fetch_file(path, test) { + info("Fetching file " + path); + return new Promise((resolve, reject) => { + let file = new FileUtils.File(path); + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true, + }, + function(stream, status) { + if (!Components.isSuccessCode(status)) { + reject(status); + return; + } + let result, reject; + try { + result = NetUtil.readInputStreamToString(stream, stream.available()); + } catch (x) { + reject = x; + } + stream.close(); + if (reject) { + reject(reject); + } else { + resolve(result); + } + } + ); + }); +} + +/** + * Compare asynchronously the contents two files using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} a The _absolute_ path to the first file. + * @param {string} b The _absolute_ path to the second file. + * + * @resolves {null} + */ +function reference_compare_files(a, b, test) { + return (async function() { + info("Comparing files " + a + " and " + b); + let a_contents = await reference_fetch_file(a, test); + let b_contents = await reference_fetch_file(b, test); + Assert.equal(a_contents, b_contents); + })(); +} + +async function removeTestFile(filePath, ignoreNoSuchFile = true) { + try { + await OS.File.remove(filePath); + } catch (ex) { + if (!ignoreNoSuchFile || !ex.becauseNoSuchFile) { + do_throw(ex); + } + } +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_compression.js b/toolkit/components/osfile/tests/xpcshell/test_compression.js new file mode 100644 index 0000000000..2daa4c7891 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_compression.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +add_task(async function test_compress_lz4() { + let path = OS.Path.join(OS.Constants.Path.tmpDir, "compression.lz"); + let length = 1024; + let array = new Uint8Array(length); + for (let i = 0; i < array.byteLength; ++i) { + array[i] = i; + } + let arrayAsString = Array.prototype.join.call(array); + + info("Writing data with lz4 compression"); + let bytes = await OS.File.writeAtomic(path, array, { compression: "lz4" }); + info("Compressed " + length + " bytes into " + bytes); + + info("Reading back with lz4 decompression"); + let decompressed = await OS.File.read(path, { compression: "lz4" }); + info("Decompressed into " + decompressed.byteLength + " bytes"); + Assert.equal(arrayAsString, Array.prototype.join.call(decompressed)); +}); + +add_task(async function test_uncompressed() { + info("Writing data without compression"); + let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_compression.tmp"); + let array = new Uint8Array(1024); + for (let i = 0; i < array.byteLength; ++i) { + array[i] = i; + } + await OS.File.writeAtomic(path, array); // No compression + + let exn; + // Force decompression, reading should fail + try { + await OS.File.read(path, { compression: "lz4" }); + } catch (ex) { + exn = ex; + } + Assert.ok(!!exn); + // Check the exception message (and that it contains the file name) + Assert.ok( + exn.message.includes(`Invalid header (no magic number) - Data: ${path}`) + ); +}); + +add_task(async function test_no_header() { + let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_header.tmp"); + let array = new Uint8Array(8).fill(0, 0); // Small array with no header + + info("Writing data with no header"); + + await OS.File.writeAtomic(path, array); // No compression + let exn; + // Force decompression, reading should fail + try { + await OS.File.read(path, { compression: "lz4" }); + } catch (ex) { + exn = ex; + } + Assert.ok(!!exn); + // Check the exception message (and that it contains the file name) + Assert.ok( + exn.message.includes(`Buffer is too short (no header) - Data: ${path}`) + ); +}); + +add_task(async function test_invalid_content() { + let path = OS.Path.join(OS.Constants.Path.tmpDir, "invalid_content.tmp"); + let arr1 = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]); + let arr2 = new Uint8Array(248).fill(1, 0); + + let array = new Uint8Array(arr1.length + arr2.length); + array.set(arr1); + array.set(arr2, arr1.length); + + info("Writing invalid data (with a valid header and only ones after that)"); + + await OS.File.writeAtomic(path, array); // No compression + let exn; + // Force decompression, reading should fail + try { + await OS.File.read(path, { compression: "lz4" }); + } catch (ex) { + exn = ex; + } + Assert.ok(!!exn); + // Check the exception message (and that it contains the file name) + Assert.ok( + exn.message.includes( + `Invalid content: Decompression stopped at 0 - Data: ${path}` + ) + ); +}); + +add_task(function() { + do_test_finished(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_constants.js b/toolkit/components/osfile/tests/xpcshell/test_constants.js new file mode 100644 index 0000000000..7bc2e72a07 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_constants.js @@ -0,0 +1,20 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// Test that OS.Constants is defined correctly. +add_task(async function check_definition() { + Assert.ok(OS.Constants != null); + Assert.ok(!!OS.Constants.Win || !!OS.Constants.libc); + Assert.ok(OS.Constants.Path != null); + Assert.ok(OS.Constants.Sys != null); + // check system name + Assert.equal(Services.appinfo.OS, OS.Constants.Sys.Name); + + // check if using DEBUG build + if (Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2).isDebugBuild) { + Assert.ok(OS.Constants.Sys.DEBUG); + } else { + Assert.ok(typeof OS.Constants.Sys.DEBUG == "undefined"); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_duration.js b/toolkit/components/osfile/tests/xpcshell/test_duration.js new file mode 100644 index 0000000000..9c2b54a4b6 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_duration.js @@ -0,0 +1,127 @@ +var { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Test optional duration reporting that can be used for telemetry. + */ +add_task(async function duration() { + const availableDurations = [ + "outSerializationDuration", + "outExecutionDuration", + ]; + Services.prefs.setBoolPref("toolkit.osfile.log", true); + // Options structure passed to a OS.File copy method. + let copyOptions = { + // These fields should be overwritten with the actual duration + // measurements. + outSerializationDuration: null, + outExecutionDuration: null, + }; + let currentDir = await OS.File.getCurrentDirectory(); + let pathSource = OS.Path.join(currentDir, "test_duration.js"); + let copyFile = pathSource + ".bak"; + function testOptions(options, name, durations = availableDurations) { + for (let duration of durations) { + info(`Checking ${duration} for operation: ${name}`); + info(`${name}: Gathered method duration time: ${options[duration]} ms`); + // Making sure that duration was updated. + Assert.equal(typeof options[duration], "number"); + Assert.ok(options[duration] >= 0); + } + } + + function testOptionIncrements( + options, + name, + backupDuration, + durations = availableDurations + ) { + for (let duration of durations) { + info(`Checking ${duration} increment for operation: ${name}`); + info(`${name}: Gathered method duration time: ${options[duration]} ms`); + info(`${name}: Previous duration: ${backupDuration[duration]} ms`); + // Making sure that duration was incremented. + Assert.ok(options[duration] >= backupDuration[duration]); + } + } + + // Testing duration of OS.File.copy. + await OS.File.copy(pathSource, copyFile, copyOptions); + testOptions(copyOptions, "OS.File.copy"); + await OS.File.remove(copyFile); + + // Trying an operation where options are cloned. + let pathDest = OS.Path.join( + OS.Constants.Path.tmpDir, + "osfile async test read writeAtomic.tmp" + ); + let tmpPath = pathDest + ".tmp"; + let readOptions = { + // We do not check for |outSerializationDuration| since |Scheduler.post| + // may not be called whenever |read| is called. + outExecutionDuration: null, + }; + let contents = await OS.File.read(pathSource, undefined, readOptions); + testOptions(readOptions, "OS.File.read", ["outExecutionDuration"]); + // Options structure passed to a OS.File writeAtomic method. + let writeAtomicOptions = { + // This field should be first initialized with the actual + // duration measurement then progressively incremented. + outExecutionDuration: null, + tmpPath, + }; + // Note that |contents| cannot be reused after this call since it is detached. + await OS.File.writeAtomic(pathDest, contents, writeAtomicOptions); + testOptions(writeAtomicOptions, "OS.File.writeAtomic", [ + "outExecutionDuration", + ]); + await OS.File.remove(pathDest); + + info( + `Ensuring that we can use ${availableDurations.join( + ", " + )} to accumulate durations` + ); + + let ARBITRARY_BASE_DURATION = 5; + copyOptions = { + // This field should now be incremented with the actual duration + // measurement. + outSerializationDuration: ARBITRARY_BASE_DURATION, + outExecutionDuration: ARBITRARY_BASE_DURATION, + }; + + // We need to copy the object, since having a reference would make this pointless. + let backupDuration = Object.assign({}, copyOptions); + + // Testing duration of OS.File.copy. + await OS.File.copy(pathSource, copyFile, copyOptions); + testOptionIncrements(copyOptions, "copy", backupDuration); + + backupDuration = Object.assign({}, copyOptions); + await OS.File.remove(copyFile, copyOptions); + testOptionIncrements(copyOptions, "remove", backupDuration); + + // Trying an operation where options are cloned. + // Options structure passed to a OS.File writeAtomic method. + writeAtomicOptions = { + // We do not check for |outSerializationDuration| since |Scheduler.post| + // may not be called whenever |writeAtomic| is called. + outExecutionDuration: ARBITRARY_BASE_DURATION, + }; + writeAtomicOptions.tmpPath = tmpPath; + backupDuration = Object.assign({}, writeAtomicOptions); + contents = await OS.File.read(pathSource, undefined, readOptions); + await OS.File.writeAtomic(pathDest, contents, writeAtomicOptions); + testOptionIncrements( + writeAtomicOptions, + "writeAtomicOptions", + backupDuration, + ["outExecutionDuration"] + ); + OS.File.remove(pathDest); + + // Testing an operation that doesn't take arguments at all + let file = await OS.File.open(pathSource); + await file.stat(); + await file.close(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_exception.js b/toolkit/components/osfile/tests/xpcshell/test_exception.js new file mode 100644 index 0000000000..5a4ffc8441 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_exception.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that functions throw the appropriate exceptions. + */ + +"use strict"; + +var EXISTING_FILE = do_get_file("xpcshell.ini").path; + +// Tests on |open| + +add_test_pair(async function test_typeerror() { + let exn; + try { + let fd = await OS.File.open("/tmp", { no_such_key: 1 }); + info("Fd: " + fd); + } catch (ex) { + exn = ex; + } + info("Exception: " + exn); + Assert.ok(exn.constructor.name == "TypeError"); +}); + +// Tests on |read| + +add_test_pair(async function test_bad_encoding() { + info("Testing with a wrong encoding"); + try { + await OS.File.read(EXISTING_FILE, { encoding: "baby-speak-encoded" }); + do_throw("Should have thrown with an ex.becauseInvalidArgument"); + } catch (ex) { + if (ex.becauseInvalidArgument) { + info("Wrong encoding caused the correct exception"); + } else { + throw ex; + } + } + + try { + await OS.File.read(EXISTING_FILE, { encoding: 4 }); + do_throw("Should have thrown a TypeError"); + } catch (ex) { + if (ex.constructor.name == "TypeError") { + // Note that TypeError doesn't carry across compartments + info("Non-string encoding caused the correct exception"); + } else { + throw ex; + } + } +}); + +add_test_pair(async function test_bad_compression() { + info("Testing with a non-existing compression"); + try { + await OS.File.read(EXISTING_FILE, { compression: "mmmh-crunchy" }); + do_throw("Should have thrown with an ex.becauseInvalidArgument"); + } catch (ex) { + if (ex.becauseInvalidArgument) { + info("Wrong encoding caused the correct exception"); + } else { + throw ex; + } + } + + info("Testing with a bad type for option compression"); + try { + await OS.File.read(EXISTING_FILE, { compression: 5 }); + do_throw("Should have thrown a TypeError"); + } catch (ex) { + if (ex.constructor.name == "TypeError") { + // Note that TypeError doesn't carry across compartments + info("Non-string encoding caused the correct exception"); + } else { + throw ex; + } + } +}); + +add_test_pair(async function test_bad_bytes() { + info("Testing with a bad type for option bytes"); + try { + await OS.File.read(EXISTING_FILE, { bytes: "five" }); + do_throw("Should have thrown a TypeError"); + } catch (ex) { + if (ex.constructor.name == "TypeError") { + // Note that TypeError doesn't carry across compartments + info("Non-number bytes caused the correct exception"); + } else { + throw ex; + } + } +}); + +add_test_pair(async function read_non_existent() { + info("Testing with a non-existent file"); + try { + await OS.File.read("I/do/not/exist"); + do_throw("Should have thrown with an ex.becauseNoSuchFile"); + } catch (ex) { + if (ex.becauseNoSuchFile) { + info("Correct exceptions"); + } else { + throw ex; + } + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js new file mode 100644 index 0000000000..41b57414e0 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + let isWindows = "@mozilla.org/windows-registry-key;1" in Cc; + + // Test cases for filePathToURI + let paths = isWindows + ? [ + "C:\\", + "C:\\test", + "C:\\test\\", + "C:\\test%2f", + "C:\\test\\test\\test", + "C:\\test;+%", + "C:\\test?action=index\\", + "C:\\test test", + "\\\\C:\\a\\b\\c", + "\\\\Server\\a\\b\\c", + + // note that per http://support.microsoft.com/kb/177506 (under more info), + // the following characters are allowed on Windows: + "C:\\char^", + "C:\\char&", + "C:\\char'", + "C:\\char@", + "C:\\char{", + "C:\\char}", + "C:\\char[", + "C:\\char]", + "C:\\char,", + "C:\\char$", + "C:\\char=", + "C:\\char!", + "C:\\char-", + "C:\\char#", + "C:\\char(", + "C:\\char)", + "C:\\char%", + "C:\\char.", + "C:\\char+", + "C:\\char~", + "C:\\char_", + ] + : [ + "/", + "/test", + "/test/", + "/test%2f", + "/test/test/test", + "/test;+%", + "/test?action=index/", + "/test test", + "/punctuation/;,/?:@&=+$-_.!~*'()[]\"#", + "/CasePreserving", + ]; + + // some additional URIs to test, beyond those generated from paths + let uris = isWindows + ? [ + "file:///C:/test/", + "file://localhost/C:/test", + "file:///c:/test/test.txt", + // 'file:///C:/foo%2f', // trailing, encoded slash + "file:///C:/%3f%3F", + "file:///C:/%3b%3B", + "file:///C:/%3c%3C", // not one of the special-cased ? or ; + "file:///C:/%78", // 'x', not usually uri encoded + "file:///C:/test#frag", // a fragment identifier + "file:///C:/test?action=index", // an actual query component + ] + : [ + "file:///test/", + "file://localhost/test", + "file:///test/test.txt", + "file:///foo%2f", // trailing, encoded slash + "file:///%3f%3F", + "file:///%3b%3B", + "file:///%3c%3C", // not one of the special-cased ? or ; + "file:///%78", // 'x', not usually uri encoded + "file:///test#frag", // a fragment identifier + "file:///test?action=index", // an actual query component + ]; + + for (let path of paths) { + // convert that to a uri using FileUtils and Services, which toFileURI is trying to model + let file = FileUtils.File(path); + let uri = Services.io.newFileURI(file).spec; + Assert.equal(uri, OS.Path.toFileURI(path)); + + // keep the resulting URI to try the reverse, except for "C:\" for which the + // behavior of nsIFileURL and OS.File is inconsistent + if (path != "C:\\") { + uris.push(uri); + } + } + + for (let uri of uris) { + // convert URIs to paths with nsIFileURI, which fromFileURI is trying to model + let path = Services.io.newURI(uri).QueryInterface(Ci.nsIFileURL).file.path; + Assert.equal(path, OS.Path.fromFileURI(uri)); + } + + // check that non-file URLs aren't allowed + let thrown = false; + try { + OS.Path.fromFileURI("http://test.com"); + } catch (e) { + Assert.equal(e.message, "fromFileURI expects a file URI"); + thrown = true; + } + Assert.ok(thrown); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_logging.js b/toolkit/components/osfile/tests/xpcshell/test_logging.js new file mode 100644 index 0000000000..2fa8f9dbec --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_logging.js @@ -0,0 +1,73 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Tests logging by passing OS.Shared.LOG both an object with its own + * toString method, and one with the default. + */ +function run_test() { + do_test_pending(); + let messageCount = 0; + + info("Test starting"); + + // Create a console listener. + let consoleListener = { + observe(aMessage) { + // Ignore unexpected messages. + if (!(aMessage instanceof Ci.nsIConsoleMessage)) { + return; + } + // This is required, as printing to the |Services.console| + // while in the observe function causes an exception. + executeSoon(function() { + info("Observing message " + aMessage.message); + if (!aMessage.message.includes("TEST OS")) { + return; + } + + ++messageCount; + if (messageCount == 1) { + Assert.equal(aMessage.message, 'TEST OS {"name":"test"}\n'); + } + if (messageCount == 2) { + Assert.equal(aMessage.message, "TEST OS name is test\n"); + toggleConsoleListener(false); + do_test_finished(); + } + }); + }, + }; + + // Set/Unset the console listener. + function toggleConsoleListener(pref) { + info("Setting console listener: " + pref); + Services.prefs.setBoolPref("toolkit.osfile.log", pref); + Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref); + Services.console[pref ? "registerListener" : "unregisterListener"]( + consoleListener + ); + } + + toggleConsoleListener(true); + + let objectDefault = { name: "test" }; + let CustomToString = function() { + this.name = "test"; + }; + CustomToString.prototype.toString = function() { + return "name is " + this.name; + }; + let objectCustom = new CustomToString(); + + info(OS.Shared.LOG.toSource()); + + info("Logging 1"); + OS.Shared.LOG(objectDefault); + + info("Logging 2"); + OS.Shared.LOG(objectCustom); + // Once both messages are observed OS.Shared.DEBUG, and OS.Shared.TEST + // are reset to false. +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_makeDir.js b/toolkit/components/osfile/tests/xpcshell/test_makeDir.js new file mode 100644 index 0000000000..686bff2f2a --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_makeDir.js @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +var Path = OS.Path; +var profileDir; + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +/** + * Test OS.File.makeDir + */ + +add_task(function init() { + // Set up profile. We create the directory in the profile, because the profile + // is removed after every test run. + do_get_profile(); + profileDir = OS.Constants.Path.profileDir; + Services.prefs.setBoolPref("toolkit.osfile.log", true); +}); + +/** + * Basic use + */ + +add_task(async function test_basic() { + let dir = Path.join(profileDir, "directory"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Make a directory + await OS.File.makeDir(dir); + + // check if the directory exists + await OS.File.stat(dir); + + // Make a directory that already exists, this should succeed + await OS.File.makeDir(dir); + + // Make a directory with ignoreExisting + await OS.File.makeDir(dir, { ignoreExisting: true }); + + // Make a directory with ignoreExisting false + let exception = null; + try { + await OS.File.makeDir(dir, { ignoreExisting: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseExists); +}); + +// Make a root directory that already exists +add_task(async function test_root() { + if (OS.Constants.Win) { + await OS.File.makeDir("C:"); + await OS.File.makeDir("C:\\"); + } else { + await OS.File.makeDir("/"); + } +}); + +/** + * Creating subdirectories + */ +add_task(async function test_option_from() { + let dir = Path.join(profileDir, "a", "b", "c"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Make a directory + await OS.File.makeDir(dir, { from: profileDir }); + + // check if the directory exists + await OS.File.stat(dir); + + // Make a directory that already exists, this should succeed + await OS.File.makeDir(dir); + + // Make a directory with ignoreExisting + await OS.File.makeDir(dir, { ignoreExisting: true }); + + // Make a directory with ignoreExisting false + let exception = null; + try { + await OS.File.makeDir(dir, { ignoreExisting: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseExists); + + // Make a directory without |from| and fail + let dir2 = Path.join(profileDir, "g", "h", "i"); + exception = null; + try { + await OS.File.makeDir(dir2); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseNoSuchFile); + + // Test edge cases on paths + + let dir3 = Path.join(profileDir, "d", "", "e", "f"); + Assert.equal(false, await OS.File.exists(dir3)); + await OS.File.makeDir(dir3, { from: profileDir }); + Assert.ok(await OS.File.exists(dir3)); + + let dir4; + if (OS.Constants.Win) { + // Test that we can create a directory recursively even + // if we have too many "\\". + dir4 = profileDir + "\\\\g"; + } else { + dir4 = profileDir + "////g"; + } + Assert.equal(false, await OS.File.exists(dir4)); + await OS.File.makeDir(dir4, { from: profileDir }); + Assert.ok(await OS.File.exists(dir4)); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_open.js b/toolkit/components/osfile/tests/xpcshell/test_open.js new file mode 100644 index 0000000000..6b0c6d8b90 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_open.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Test OS.File.open for reading: + * - with an existing file (should succeed); + * - with a non-existing file (should fail); + * - with inconsistent arguments (should fail). + */ +add_task(async function() { + // Attempt to open a file that does not exist, ensure that it yields the + // appropriate error. + try { + await OS.File.open(OS.Path.join(".", "This file does not exist")); + Assert.ok(false, "File opening 1 succeeded (it should fail)"); + } catch (err) { + if (err instanceof OS.File.Error && err.becauseNoSuchFile) { + info("File opening 1 failed " + err); + } else { + throw err; + } + } + // Attempt to open a file with the wrong args, so that it fails before + // serialization, ensure that it yields the appropriate error. + info("Attempting to open a file with wrong arguments"); + try { + let fd = await OS.File.open(1, 2, 3); + Assert.ok(false, "File opening 2 succeeded (it should fail)" + fd); + } catch (err) { + info("File opening 2 failed " + err); + Assert.equal( + false, + err instanceof OS.File.Error, + "File opening 2 returned something that is not a file error" + ); + Assert.ok( + err.constructor.name == "TypeError", + "File opening 2 returned a TypeError" + ); + } + + // Attempt to open a file correctly + info("Attempting to open a file correctly"); + let openedFile = await OS.File.open( + OS.Path.join(do_get_cwd().path, "test_open.js") + ); + info("File opened correctly"); + + info("Attempting to close a file correctly"); + await openedFile.close(); + + info("Attempting to close a file again"); + await openedFile.close(); +}); + +/** + * Test the error thrown by OS.File.open when attempting to open a directory + * that does not exist. + */ +add_task(async function test_error_attributes() { + let dir = OS.Path.join(do_get_profile().path, "test_osfileErrorAttrs"); + let fpath = OS.Path.join(dir, "test_error_attributes.txt"); + + try { + await OS.File.open(fpath, { truncate: true }, {}); + Assert.ok(false, "Opening path suceeded (it should fail) " + fpath); + } catch (err) { + Assert.ok(err instanceof OS.File.Error); + Assert.ok(err.becauseNoSuchFile); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js new file mode 100644 index 0000000000..7c2e2db06b --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js @@ -0,0 +1,13 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A trivial test ensuring that we can call osfile from xpcshell. + * (see bug 808161) + */ + +function run_test() { + do_test_pending(); + OS.File.getCurrentDirectory().then(do_test_finished, do_test_finished); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js new file mode 100644 index 0000000000..8fabbad1ed --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js @@ -0,0 +1,105 @@ +"use strict"; + +info("starting tests"); + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A test to check that the |append| mode flag is correctly implemented. + * (see bug 925865) + */ + +function setup_mode(mode) { + // Complete mode. + let realMode = { + read: true, + write: true, + }; + for (let k in mode) { + realMode[k] = mode[k]; + } + return realMode; +} + +// Test append mode. +async function test_append(mode) { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_append.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + mode = setup_mode(mode); + mode.append = true; + if (mode.trunc) { + // Pre-fill file with some data to see if |trunc| actually works. + await OS.File.writeAtomic(path, new Uint8Array(500)); + } + let file = await OS.File.open(path, mode); + try { + await file.write(new Uint8Array(1000)); + await file.setPosition(0, OS.File.POS_START); + await file.read(100); + // Should be at offset 100, length 1000 now. + await file.write(new Uint8Array(100)); + // Should be at offset 1100, length 1100 now. + let stat = await file.stat(); + Assert.equal(1100, stat.size); + } finally { + await file.close(); + } + } catch (ex) { + await removeTestFile(path); + } +} + +// Test no-append mode. +async function test_no_append(mode) { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_noappend.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + mode = setup_mode(mode); + mode.append = false; + if (mode.trunc) { + // Pre-fill file with some data to see if |trunc| actually works. + await OS.File.writeAtomic(path, new Uint8Array(500)); + } + let file = await OS.File.open(path, mode); + try { + await file.write(new Uint8Array(1000)); + await file.setPosition(0, OS.File.POS_START); + await file.read(100); + // Should be at offset 100, length 1000 now. + await file.write(new Uint8Array(100)); + // Should be at offset 200, length 1000 now. + let stat = await file.stat(); + Assert.equal(1000, stat.size); + } finally { + await file.close(); + } + } finally { + await removeTestFile(path); + } +} + +var test_flags = [{}, { create: true }, { trunc: true }]; +function run_test() { + do_test_pending(); + + for (let t of test_flags) { + add_task(test_append.bind(null, t)); + add_task(test_no_append.bind(null, t)); + } + add_task(do_test_finished); + + run_next_test(); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js new file mode 100644 index 0000000000..6441c88112 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js @@ -0,0 +1,40 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +/** + * Test to ensure that {bytes:} in options to |write| is correctly + * preserved. + */ +add_task(async function test_bytes() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_bytes.tmp" + ); + let file = await OS.File.open(path, { trunc: true, read: true, write: true }); + try { + try { + // 1. Test write, by supplying {bytes:} options smaller than the actual + // buffer. + await file.write(new Uint8Array(2048), { bytes: 1024 }); + Assert.equal((await file.stat()).size, 1024); + + // 2. Test that passing nullish values for |options| still works. + await file.setPosition(0, OS.File.POS_END); + await file.write(new Uint8Array(1024), null); + await file.write(new Uint8Array(1024), undefined); + Assert.equal((await file.stat()).size, 3072); + } finally { + await file.close(); + } + } finally { + await OS.File.remove(path); + } +}); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js new file mode 100644 index 0000000000..0c82e542f6 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js @@ -0,0 +1,109 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +/** + * A file that we know exists and that can be used for reading. + */ +var EXISTING_FILE = "test_osfile_async_copy.js"; + +/** + * Fetch asynchronously the contents of a file using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} path The _absolute_ path to the file. + * @return {promise} + * @resolves {string} The contents of the file. + */ +var reference_fetch_file = function reference_fetch_file(path) { + return new Promise((resolve, reject) => { + let file = new FileUtils.File(path); + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true, + }, + function(stream, status) { + if (!Components.isSuccessCode(status)) { + reject(status); + return; + } + let result, reject; + try { + result = NetUtil.readInputStreamToString(stream, stream.available()); + } catch (x) { + reject = x; + } + stream.close(); + if (reject) { + reject(reject); + } else { + resolve(result); + } + } + ); + }); +}; + +/** + * Compare asynchronously the contents two files using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} a The _absolute_ path to the first file. + * @param {string} b The _absolute_ path to the second file. + * + * @resolves {null} + */ +var reference_compare_files = async function reference_compare_files(a, b) { + let a_contents = await reference_fetch_file(a); + let b_contents = await reference_fetch_file(b); + // Not using do_check_eq to avoid dumping the whole file to the log. + // It is OK to === compare here, as both variables contain a string. + Assert.ok(a_contents === b_contents); +}; + +/** + * Test to ensure that OS.File.copy works. + */ +async function test_copymove(options = {}) { + let source = OS.Path.join(await OS.File.getCurrentDirectory(), EXISTING_FILE); + let dest = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_copy_dest.tmp" + ); + let dest2 = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_copy_dest2.tmp" + ); + try { + // 1. Test copy. + await OS.File.copy(source, dest, options); + await reference_compare_files(source, dest); + // 2. Test subsequent move. + await OS.File.move(dest, dest2); + await reference_compare_files(source, dest2); + // 3. Check that the moved file was really moved. + Assert.equal(await OS.File.exists(dest), false); + } finally { + await removeTestFile(dest); + await removeTestFile(dest2); + } +} + +// Regular copy test. +add_task(test_copymove); +// Userland copy test. +add_task(test_copymove.bind(null, { unixUserland: true })); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js new file mode 100644 index 0000000000..e1b377f3c7 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js @@ -0,0 +1,31 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +/** + * Test to ensure that |File.prototype.flush| is available in the async API. + */ + +add_task(async function test_flush() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_flush.tmp" + ); + let file = await OS.File.open(path, { trunc: true, write: true }); + try { + try { + await file.flush(); + } finally { + await file.close(); + } + } finally { + await OS.File.remove(path); + } +}); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js new file mode 100644 index 0000000000..5af887c045 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A test to check that .getPosition/.setPosition work with large files. + * (see bug 952997) + */ + +// Test setPosition/getPosition. +async function test_setPosition(forward, current, backward) { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_largefiles.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + let file = await OS.File.open(path, { write: true, append: false }); + try { + let pos = 0; + + // 1. seek forward from start + info("Moving forward: " + forward); + await file.setPosition(forward, OS.File.POS_START); + pos += forward; + Assert.equal(await file.getPosition(), pos); + + // 2. seek forward from current position + info("Moving current: " + current); + await file.setPosition(current, OS.File.POS_CURRENT); + pos += current; + Assert.equal(await file.getPosition(), pos); + + // 3. seek backward from current position + info("Moving current backward: " + backward); + await file.setPosition(-backward, OS.File.POS_CURRENT); + pos -= backward; + Assert.equal(await file.getPosition(), pos); + } finally { + await file.setPosition(0, OS.File.POS_START); + await file.close(); + } + } catch (ex) { + await removeTestFile(path); + } +} + +// Test setPosition/getPosition expected failures. +async function test_setPosition_failures() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_largefiles.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + let file = await OS.File.open(path, { write: true, append: false }); + try { + // 1. Use an invalid position value + try { + await file.setPosition(0.5, OS.File.POS_START); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + Assert.ok(ex.toString().includes("can't pass")); + } + // Since setPosition should have bailed, it shouldn't have moved the + // file pointer at all. + Assert.equal(await file.getPosition(), 0); + + // 2. Use an invalid position value + try { + await file.setPosition(0xffffffff + 0.5, OS.File.POS_START); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + Assert.ok(ex.toString().includes("can't pass")); + } + // Since setPosition should have bailed, it shouldn't have moved the + // file pointer at all. + Assert.equal(await file.getPosition(), 0); + + // 3. Use a position that cannot be represented as a double + try { + // Not all numbers after 9007199254740992 can be represented as a + // double. E.g. in js 9007199254740992 + 1 == 9007199254740992 + await file.setPosition(9007199254740992, OS.File.POS_START); + await file.setPosition(1, OS.File.POS_CURRENT); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + info(ex.toString()); + Assert.ok(!!ex); + } + } finally { + await file.setPosition(0, OS.File.POS_START); + await file.close(); + await removeTestFile(path); + } + } catch (ex) { + do_throw(ex); + } +} + +function run_test() { + // First verify stuff works for small values. + add_task(test_setPosition.bind(null, 0, 100, 50)); + add_task(test_setPosition.bind(null, 1000, 100, 50)); + add_task(test_setPosition.bind(null, 1000, -100, -50)); + + if (OS.Constants.Win || ctypes.off_t.size >= 8) { + // Now verify stuff still works for large values. + // 1. Multiple small seeks, which add up to > MAXINT32 + add_task(test_setPosition.bind(null, 0x7fffffff, 0x7fffffff, 0)); + // 2. Plain large seek, that should end up at 0 again. + // 0xffffffff also happens to be the INVALID_SET_FILE_POINTER value on + // Windows, so this also tests the error handling + add_task(test_setPosition.bind(null, 0, 0xffffffff, 0xffffffff)); + // 3. Multiple large seeks that should end up > MAXINT32. + add_task(test_setPosition.bind(null, 0xffffffff, 0xffffffff, 0xffffffff)); + // 5. Multiple large seeks with negative offsets. + add_task(test_setPosition.bind(null, 0xffffffff, -0x7fffffff, 0x7fffffff)); + + // 6. Check failures + add_task(test_setPosition_failures); + } + + run_next_test(); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js new file mode 100644 index 0000000000..b60c448cee --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js @@ -0,0 +1,214 @@ +"use strict"; + +/* eslint-disable no-lone-blocks */ + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A test to ensure that OS.File.setDates and OS.File.prototype.setDates are + * working correctly. + * (see bug 924916) + */ + +// Non-prototypical tests, operating on path names. +add_task(async function test_nonproto() { + // First, create a file we can mess with. + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setDates_nonproto.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + // 1. Try to set some well known dates. + // We choose multiples of 2000ms, because the time stamp resolution of + // the underlying OS might not support something more precise. + const accDate = 2000; + const modDate = 4000; + { + await OS.File.setDates(path, accDate, modDate); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 2.1 Try to omit modificationDate (which should then default to + // |Date.now()|, expect for resolution differences). + { + await OS.File.setDates(path, accDate); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 2.2 Try to omit accessDate as well (which should then default to + // |Date.now()|, expect for resolution differences). + { + await OS.File.setDates(path); + let stat = await OS.File.stat(path); + Assert.notEqual(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 3. Repeat 1., but with Date objects this time + { + await OS.File.setDates(path, new Date(accDate), new Date(modDate)); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 4. Check that invalid params will cause an exception/rejection. + { + for (let p of ["invalid", new Uint8Array(1), NaN]) { + try { + await OS.File.setDates(path, p, modDate); + do_throw("Invalid access date should have thrown for: " + p); + } catch (ex) { + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await OS.File.setDates(path, accDate, p); + do_throw("Invalid modification date should have thrown for: " + p); + } catch (ex) { + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await OS.File.setDates(path, p, p); + do_throw("Invalid dates should have thrown for: " + p); + } catch (ex) { + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + } + } + } finally { + // Remove the temp file again + await OS.File.remove(path); + } +}); + +// Prototypical tests, operating on |File| handles. +add_task(async function test_proto() { + if (OS.Constants.Sys.Name == "Android") { + info("File.prototype.setDates is not implemented for Android"); + Assert.equal(OS.File.prototype.setDates, undefined); + return; + } + + // First, create a file we can mess with. + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setDates_proto.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + + try { + // 1. Try to set some well known dates. + // We choose multiples of 2000ms, because the time stamp resolution of + // the underlying OS might not support something more precise. + const accDate = 2000; + const modDate = 4000; + { + await fd.setDates(accDate, modDate); + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 2.1 Try to omit modificationDate (which should then default to + // |Date.now()|, expect for resolution differences). + { + await fd.setDates(accDate); + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 2.2 Try to omit accessDate as well (which should then default to + // |Date.now()|, expect for resolution differences). + { + await fd.setDates(); + let stat = await fd.stat(); + Assert.notEqual(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 3. Repeat 1., but with Date objects this time + { + await fd.setDates(new Date(accDate), new Date(modDate)); + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 4. Check that invalid params will cause an exception/rejection. + { + for (let p of ["invalid", new Uint8Array(1), NaN]) { + try { + await fd.setDates(p, modDate); + do_throw("Invalid access date should have thrown for: " + p); + } catch (ex) { + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await fd.setDates(accDate, p); + do_throw("Invalid modification date should have thrown for: " + p); + } catch (ex) { + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await fd.setDates(p, p); + do_throw("Invalid dates should have thrown for: " + p); + } catch (ex) { + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + } + } + } finally { + await fd.close(); + } + } finally { + // Remove the temp file again + await OS.File.remove(path); + } +}); + +// Tests setting dates on directories. +add_task(async function test_dirs() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setDates_dir" + ); + await OS.File.makeDir(path); + + try { + // 1. Try to set some well known dates. + // We choose multiples of 2000ms, because the time stamp resolution of + // the underlying OS might not support something more precise. + const accDate = 2000; + const modDate = 4000; + { + await OS.File.setDates(path, accDate, modDate); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + } finally { + await OS.File.removeEmptyDir(path); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js new file mode 100644 index 0000000000..97a33633ac --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * A test to ensure that OS.File.setPermissions and + * OS.File.prototype.setPermissions are all working correctly. + * (see bug 1001849) + * These functions are currently Unix-specific. The manifest skips + * the test on Windows. + */ + +/** + * Helper function for test logging: prints a POSIX file permission mode as an + * octal number, with a leading '0' per C (not JS) convention. When the + * numeric value is 0o777 or lower, it is padded on the left with zeroes to + * four digits wide. + * Sample outputs: 0022, 0644, 04755. + */ +function format_mode(mode) { + if (mode <= 0o777) { + return ("0000" + mode.toString(8)).slice(-4); + } + return "0" + mode.toString(8); +} + +const _umask = OS.Constants.Sys.umask; +info("umask: " + format_mode(_umask)); + +/** + * Compute the mode that a file should have after applying the umask, + * whatever it happens to be. + */ +function apply_umask(mode) { + return mode & ~_umask; +} + +// Sequence of setPermission parameters and expected file mode. The first test +// checks the permissions when the file is first created. +var testSequence = [ + [null, apply_umask(0o600)], + [{ unixMode: 0o4777 }, apply_umask(0o4777)], + [{ unixMode: 0o4777, unixHonorUmask: false }, 0o4777], + [{ unixMode: 0o4777, unixHonorUmask: true }, apply_umask(0o4777)], + [undefined, apply_umask(0o600)], + [{ unixMode: 0o666 }, apply_umask(0o666)], + [{ unixMode: 0o600 }, apply_umask(0o600)], + [{ unixMode: 0 }, 0], + [{}, apply_umask(0o600)], +]; + +// Test application to paths. +add_task(async function test_path_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setPermissions_path.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + for (let [options, expectedMode] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await OS.File.setPermissions(path, options); + } + + let stat = await OS.File.stat(path); + Assert.equal(format_mode(stat.unixMode), format_mode(expectedMode)); + } + } finally { + await OS.File.remove(path); + } +}); + +// Test application to open files. +add_task(async function test_file_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setPermissions_file.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + try { + for (let [options, expectedMode] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await fd.setPermissions(options); + } + + let stat = await fd.stat(); + Assert.equal(format_mode(stat.unixMode), format_mode(expectedMode)); + } + } finally { + await fd.close(); + } + } finally { + await OS.File.remove(path); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js new file mode 100644 index 0000000000..f4a1fe8455 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js @@ -0,0 +1,46 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +add_task(async function test_closed() { + OS.Shared.DEBUG = true; + let currentDir = await OS.File.getCurrentDirectory(); + info("Open a file, ensure that we can call stat()"); + let path = OS.Path.join(currentDir, "test_osfile_closed.js"); + let file = await OS.File.open(path); + await file.stat(); + Assert.ok(true); + + await file.close(); + + info("Ensure that we cannot stat() on closed file"); + let exn; + try { + await file.stat(); + } catch (ex) { + exn = ex; + } + info("Ensure that this raises the correct error"); + Assert.ok(!!exn); + Assert.ok(exn instanceof OS.File.Error); + Assert.ok(exn.becauseClosed); + + info("Ensure that we cannot read() on closed file"); + exn = null; + try { + await file.read(); + } catch (ex) { + exn = ex; + } + info("Ensure that this raises the correct error"); + Assert.ok(!!exn); + Assert.ok(exn instanceof OS.File.Error); + Assert.ok(exn.becauseClosed); +}); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js new file mode 100644 index 0000000000..0bf8cd4cb7 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { + OS: { File, Path, Constants }, +} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +add_task(async function testFileError_with_writeAtomic() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let path = Path.join(Constants.Path.tmpDir, "testFileError.tmp"); + await File.remove(path); + await File.writeAtomic(path, DEFAULT_CONTENTS); + let exception; + try { + await File.writeAtomic(path, DEFAULT_CONTENTS, { noOverwrite: true }); + } catch (ex) { + exception = ex; + } + Assert.ok(exception instanceof File.Error); + Assert.ok(exception.path == path); +}); + +add_task(async function testFileError_with_makeDir() { + let path = Path.join(Constants.Path.tmpDir, "directory"); + await File.removeDir(path); + await File.makeDir(path); + let exception; + try { + await File.makeDir(path, { ignoreExisting: false }); + } catch (ex) { + exception = ex; + } + Assert.ok(exception instanceof File.Error); + Assert.ok(exception.path == path); +}); + +add_task(async function testFileError_with_move() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let sourcePath = Path.join(Constants.Path.tmpDir, "src.tmp"); + let destPath = Path.join(Constants.Path.tmpDir, "dest.tmp"); + await File.remove(sourcePath); + await File.remove(destPath); + await File.writeAtomic(sourcePath, DEFAULT_CONTENTS); + await File.writeAtomic(destPath, DEFAULT_CONTENTS); + let exception; + try { + await File.move(sourcePath, destPath, { noOverwrite: true }); + } catch (ex) { + exception = ex; + } + info(exception); + Assert.ok(exception instanceof File.Error); + Assert.ok(exception.path == sourcePath); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js new file mode 100644 index 0000000000..376b515f76 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js @@ -0,0 +1,97 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// We want the actual global to get at the internals since Scheduler is not +// exported. +var { Scheduler } = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_async_front.jsm" +); + +/** + * Verify that Scheduler.kill() interacts with other OS.File requests correctly, + * and that no requests are lost. This is relevant because on B2G we + * auto-kill the worker periodically, making it very possible for valid requests + * to be interleaved with the automatic kill(). + * + * This test is being created with the fix for Bug 1125989 where `kill` queue + * management was found to be buggy. It is a glass-box test that explicitly + * re-creates the observed failure situation; it is not guaranteed to prevent + * all future regressions. The following is a detailed explanation of the test + * for your benefit if this test ever breaks or you are wondering what was the + * point of all this. You might want to skim the code below first. + * + * OS.File maintains a `queue` of operations to be performed. This queue is + * nominally implemented as a chain of promises. Every time a new job is + * OS.File.push()ed, it effectively becomes the new `queue` promise. (An + * extra promise is interposed with a rejection handler to avoid the rejection + * cascading, but that does not matter for our purposes.) + * + * The flaw in `kill` was that it would wait for the `queue` to complete before + * replacing `queue`. As a result, another OS.File operation could use `push` + * (by way of OS.File.post()) to also use .then() on the same `queue` promise. + * Accordingly, assuming that promise was not yet resolved (due to a pending + * OS.File request), when it was resolved, both the task scheduled in `kill` + * and in `post` would be triggered. Both of those tasks would run until + * encountering a call to worker.post(). + * + * Re-creating this race is not entirely trivial because of the large number of + * promises used by the code causing control flow to repeatedly be deferred. In + * a slightly simpler world we could run the follwing in the same turn of the + * event loop and trigger the problem. + * - any OS.File request + * - Scheduler.kill() + * - any OS.File request + * + * However, we need the Scheduler.kill task to reach the point where it is + * waiting on the same `queue` that another task has been scheduled against. + * Since the `kill` task yields on the `killQueue` promise prior to yielding + * on `queue`, however, some turns of the event loop are required. Happily, + * for us, as discussed above, the problem triggers when we have two promises + * scheduled on the `queue`, so we can just wait to schedule the second OS.File + * request on the queue. (Note that because of the additional then() added to + * eat rejections, there is an important difference between the value of + * `queue` and the value returned by the first OS.File request.) + */ +add_task(async function test_kill_race() { + // Ensure the worker has been created and that SET_DEBUG has taken effect. + // We have chosen OS.File.exists for our tests because it does not trigger + // a rejection and we absolutely do not care what the operation is other + // than it does not invoke a native fast-path. + await OS.File.exists("foo.foo"); + + info("issuing first request"); + let firstRequest = OS.File.exists("foo.bar"); // eslint-disable-line no-unused-vars + let secondRequest; + let secondResolved = false; + + // As noted in our big block comment, we want to wait to schedule the + // second request so that it races `kill`'s call to `worker.post`. Having + // ourselves wait on the same promise, `queue`, and registering ourselves + // before we issue the kill request means we will get run before the `kill` + // task resumes and allow us to precisely create the desired race. + Scheduler.queue.then(function() { + info("issuing second request"); + secondRequest = OS.File.exists("foo.baz"); + secondRequest.then(function() { + secondResolved = true; + }); + }); + + info("issuing kill request"); + let killRequest = Scheduler.kill({ reset: true, shutdown: false }); + + // Wait on the killRequest so that we can schedule a new OS.File request + // after it completes... + await killRequest; + // ...because our ordering guarantee ensures that there is at most one + // worker (and this usage here should not be vulnerable even with the + // bug present), so when this completes the secondRequest has either been + // resolved or lost. + await OS.File.exists("foo.goz"); + + ok( + secondResolved, + "The second request was resolved so we avoided the bug. Victory!" + ); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js new file mode 100644 index 0000000000..b2708274c2 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * A test to ensure that OS.File.setPermissions and + * OS.File.prototype.setPermissions are all working correctly. + * (see bug 1022816) + * The manifest tests on Windows. + */ + +// Sequence of setPermission parameters. +var testSequence = [ + [ + { winAttributes: { readOnly: true, system: true, hidden: true } }, + { readOnly: true, system: true, hidden: true }, + ], + [ + { winAttributes: { readOnly: false } }, + { readOnly: false, system: true, hidden: true }, + ], + [ + { winAttributes: { system: false } }, + { readOnly: false, system: false, hidden: true }, + ], + [ + { winAttributes: { hidden: false } }, + { readOnly: false, system: false, hidden: false }, + ], + [ + { winAttributes: { readOnly: true, system: false, hidden: false } }, + { readOnly: true, system: false, hidden: false }, + ], + [ + { winAttributes: { readOnly: false, system: true, hidden: false } }, + { readOnly: false, system: true, hidden: false }, + ], + [ + { winAttributes: { readOnly: false, system: false, hidden: true } }, + { readOnly: false, system: false, hidden: true }, + ], +]; + +// Test application to paths. +add_task(async function test_path_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_path.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + for (let [options, attributesExpected] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await OS.File.setPermissions(path, options); + } + + let stat = await OS.File.stat(path); + info("Got stat winAttributes: " + JSON.stringify(stat.winAttributes)); + + Assert.equal(stat.winAttributes.readOnly, attributesExpected.readOnly); + Assert.equal(stat.winAttributes.system, attributesExpected.system); + Assert.equal(stat.winAttributes.hidden, attributesExpected.hidden); + } + } finally { + await OS.File.remove(path); + } +}); + +// Test application to open files. +add_task(async function test_file_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_file.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + try { + for (let [options, attributesExpected] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await fd.setPermissions(options); + } + + let stat = await fd.stat(); + info("Got stat winAttributes: " + JSON.stringify(stat.winAttributes)); + Assert.equal(stat.winAttributes.readOnly, attributesExpected.readOnly); + Assert.equal(stat.winAttributes.system, attributesExpected.system); + Assert.equal(stat.winAttributes.hidden, attributesExpected.hidden); + } + } finally { + await fd.close(); + } + } finally { + await OS.File.remove(path); + } +}); + +// Test application to Check setPermissions on a non-existant file path. +add_task(async function test_non_existant_file_path_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_path.tmp" + ); + await Assert.rejects( + OS.File.setPermissions(path, { winAttributes: { readOnly: true } }), + /The system cannot find the file specified/, + "setPermissions failed as expected on a non-existant file path" + ); +}); + +// Test application to Check setPermissions on a invalid file handle. +add_task(async function test_closed_file_handle_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_path.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + await fd.close(); + await Assert.rejects( + fd.setPermissions(path, { winAttributes: { readOnly: true } }), + /The handle is invalid/, + "setPermissions failed as expected on a invalid file handle" + ); + } finally { + await OS.File.remove(path); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js new file mode 100644 index 0000000000..e3f510a2c2 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { + OS: { File, Path, Constants }, +} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Remove all temporary files and back up files, including + * test_backupTo_option_with_tmpPath.tmp + * test_backupTo_option_with_tmpPath.tmp.backup + * test_backupTo_option_without_tmpPath.tmp + * test_backupTo_option_without_tmpPath.tmp.backup + * test_non_backupTo_option.tmp + * test_non_backupTo_option.tmp.backup + * test_backupTo_option_without_destination_file.tmp + * test_backupTo_option_without_destination_file.tmp.backup + * test_backupTo_option_with_backup_file.tmp + * test_backupTo_option_with_backup_file.tmp.backup + */ +async function clearFiles() { + let files = [ + "test_backupTo_option_with_tmpPath.tmp", + "test_backupTo_option_without_tmpPath.tmp", + "test_non_backupTo_option.tmp", + "test_backupTo_option_without_destination_file.tmp", + "test_backupTo_option_with_backup_file.tmp", + ]; + for (let file of files) { + let path = Path.join(Constants.Path.tmpDir, file); + await File.remove(path); + await File.remove(path + ".backup"); + } +} + +add_task(async function init() { + await clearFiles(); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| specified + * destination file exists + * @result destination file will be backed up + */ +add_task(async function test_backupTo_option_with_tmpPath() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_with_tmpPath.tmp" + ); + await File.writeAtomic(path, DEFAULT_CONTENTS); + await File.writeAtomic(path, WRITE_CONTENTS, { + tmpPath: path + ".tmp", + backupTo: path + ".backup", + }); + Assert.ok(await File.exists(path + ".backup")); + let contents = await File.read(path + ".backup"); + Assert.equal(DEFAULT_CONTENTS, new TextDecoder().decode(contents)); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| not specified + * destination file exists + * @result destination file will be backed up + */ +add_task(async function test_backupTo_option_without_tmpPath() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_without_tmpPath.tmp" + ); + await File.writeAtomic(path, DEFAULT_CONTENTS); + await File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" }); + Assert.ok(await File.exists(path + ".backup")); + let contents = await File.read(path + ".backup"); + Assert.equal(DEFAULT_CONTENTS, new TextDecoder().decode(contents)); +}); + +/** + * test when + * |backupTo| not specified + * |tmpPath| not specified + * destination file exists + * @result destination file will not be backed up + */ +add_task(async function test_non_backupTo_option() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join(Constants.Path.tmpDir, "test_non_backupTo_option.tmp"); + await File.writeAtomic(path, DEFAULT_CONTENTS); + await File.writeAtomic(path, WRITE_CONTENTS); + Assert.equal(false, await File.exists(path + ".backup")); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| not specified + * destination file not exists + * @result no back up file exists + */ +add_task(async function test_backupTo_option_without_destination_file() { + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_without_destination_file.tmp" + ); + await File.remove(path); + await File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" }); + Assert.equal(false, await File.exists(path + ".backup")); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| not specified + * backup file exists + * destination file exists + * @result destination file will be backed up + */ +add_task(async function test_backupTo_option_with_backup_file() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_with_backup_file.tmp" + ); + await File.writeAtomic(path, DEFAULT_CONTENTS); + + await File.writeAtomic(path + ".backup", new Uint8Array(1000)); + + await File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" }); + Assert.ok(await File.exists(path + ".backup")); + let contents = await File.read(path + ".backup"); + Assert.equal(DEFAULT_CONTENTS, new TextDecoder().decode(contents)); +}); + +add_task(async function cleanup() { + await clearFiles(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_unicode_filename.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_unicode_filename.js new file mode 100644 index 0000000000..72d23e6909 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_unicode_filename.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks against failures that may occur while creating and/or + * renaming files with Unicode paths on Windows. + * See bug 1063635#c89 for a failure due to a Unicode filename being renamed. + */ + +"use strict"; +var profileDir; + +async function writeAndCheck(path, tmpPath) { + const encoder = new TextEncoder(); + const content = "tmpContent"; + const outBin = encoder.encode(content); + await OS.File.writeAtomic(path, outBin, { tmpPath }); + + const decoder = new TextDecoder(); + const writtenBin = await OS.File.read(path); + const written = decoder.decode(writtenBin); + + // Clean up + await OS.File.remove(path); + Assert.equal( + written, + content, + `Expected correct write/read for ${path} with tmpPath ${tmpPath}` + ); +} + +add_task(async function init() { + do_get_profile(); + profileDir = OS.Constants.Path.profileDir; +}); + +add_test_pair(async function test_osfile_writeAtomic_unicode_filename() { + await writeAndCheck(OS.Path.join(profileDir, "☕") + ".tmp", undefined); + await writeAndCheck(OS.Path.join(profileDir, "☕"), undefined); + await writeAndCheck( + OS.Path.join(profileDir, "☕") + ".tmp", + OS.Path.join(profileDir, "☕") + ); + await writeAndCheck( + OS.Path.join(profileDir, "☕"), + OS.Path.join(profileDir, "☕") + ".tmp" + ); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js new file mode 100644 index 0000000000..eeaac80306 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +var SHARED_PATH; + +add_task(async function init() { + do_get_profile(); + SHARED_PATH = OS.Path.join( + OS.Constants.Path.profileDir, + "test_osfile_write_zerobytes.tmp" + ); +}); + +add_test_pair(async function test_osfile_writeAtomic_zerobytes() { + let encoder = new TextEncoder(); + let string1 = ""; + let outbin = encoder.encode(string1); + await OS.File.writeAtomic(SHARED_PATH, outbin); + + let decoder = new TextDecoder(); + let bin = await OS.File.read(SHARED_PATH); + let string2 = decoder.decode(bin); + // Checking if writeAtomic supports writing encoded zero-byte strings + Assert.equal(string2, string1, "Read the expected (empty) string."); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_path.js b/toolkit/components/osfile/tests/xpcshell/test_path.js new file mode 100644 index 0000000000..8a945f6764 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_path.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Services.prefs.setBoolPref("toolkit.osfile.test.syslib_necessary", false); +// We don't need libc/kernel32.dll for this test + +const Win = ChromeUtils.import("resource://gre/modules/osfile/ospath_win.jsm"); +const Unix = ChromeUtils.import( + "resource://gre/modules/osfile/ospath_unix.jsm" +); + +function do_check_fail(f) { + try { + let result = f(); + info("Failed do_check_fail: " + result); + Assert.ok(false); + } catch (ex) { + Assert.ok(true); + } +} + +function run_test() { + info("Testing Windows paths"); + + info("Backslash-separated, no drive"); + Assert.equal(Win.basename("a\\b"), "b"); + Assert.equal(Win.basename("a\\b\\"), ""); + Assert.equal(Win.basename("abc"), "abc"); + Assert.equal(Win.dirname("a\\b"), "a"); + Assert.equal(Win.dirname("a\\b\\"), "a\\b"); + Assert.equal(Win.dirname("a\\\\\\\\b"), "a"); + Assert.equal(Win.dirname("abc"), "."); + Assert.equal(Win.normalize("\\a\\b\\c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c"); + Assert.equal(Win.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f"); + Assert.equal(Win.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f"); + do_check_fail(() => Win.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f")); + + Assert.equal( + Win.join("\\tmp", "foo", "bar"), + "\\tmp\\foo\\bar", + "join \\tmp,foo,bar" + ); + Assert.equal( + Win.join("\\tmp", "\\foo", "bar"), + "\\foo\\bar", + "join \\tmp,\\foo,bar" + ); + Assert.equal(Win.winGetDrive("\\tmp"), null); + Assert.equal(Win.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null); + Assert.equal(Win.winGetDrive("\\"), null); + + info("Backslash-separated, with a drive"); + Assert.equal(Win.basename("c:a\\b"), "b"); + Assert.equal(Win.basename("c:a\\b\\"), ""); + Assert.equal(Win.basename("c:abc"), "abc"); + Assert.equal(Win.dirname("c:a\\b"), "c:a"); + Assert.equal(Win.dirname("c:a\\b\\"), "c:a\\b"); + Assert.equal(Win.dirname("c:a\\\\\\\\b"), "c:a"); + Assert.equal(Win.dirname("c:abc"), "c:"); + let options = { + winNoDrive: true, + }; + Assert.equal(Win.dirname("c:a\\b", options), "a"); + Assert.equal(Win.dirname("c:a\\b\\", options), "a\\b"); + Assert.equal(Win.dirname("c:a\\\\\\\\b", options), "a"); + Assert.equal(Win.dirname("c:abc", options), "."); + Assert.equal(Win.join("c:", "abc"), "c:\\abc", "join c:,abc"); + + Assert.equal(Win.normalize("c:"), "c:\\"); + Assert.equal(Win.normalize("c:\\"), "c:\\"); + Assert.equal(Win.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c"); + Assert.equal( + Win.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), + "c:\\d\\e\\f" + ); + Assert.equal(Win.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f"); + do_check_fail(() => Win.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f")); + + Assert.equal(Win.join("c:\\", "foo"), "c:\\foo", "join c:,foo"); + Assert.equal( + Win.join("c:\\tmp", "foo", "bar"), + "c:\\tmp\\foo\\bar", + "join c:\\tmp,foo,bar" + ); + Assert.equal( + Win.join("c:\\tmp", "\\foo", "bar"), + "c:\\foo\\bar", + "join c:\\tmp,\\foo,bar" + ); + Assert.equal( + Win.join("c:\\tmp", "c:\\foo", "bar"), + "c:\\foo\\bar", + "join c:\\tmp,c:\\foo,bar" + ); + Assert.equal( + Win.join("c:\\tmp", "c:foo", "bar"), + "c:\\foo\\bar", + "join c:\\tmp,c:foo,bar" + ); + Assert.equal(Win.winGetDrive("c:"), "c:"); + Assert.equal(Win.winGetDrive("c:\\"), "c:"); + Assert.equal(Win.winGetDrive("c:abc"), "c:"); + Assert.equal(Win.winGetDrive("c:abc\\d\\e\\f\\g"), "c:"); + Assert.equal(Win.winGetDrive("c:\\abc"), "c:"); + Assert.equal(Win.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:"); + + info("Forwardslash-separated, no drive"); + Assert.equal(Win.normalize("/a/b/c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("/a/b////c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("/a/b/c///"), "\\a\\b\\c"); + Assert.equal(Win.normalize("/a/b/c/../../../d/e/f"), "\\d\\e\\f"); + Assert.equal(Win.normalize("a/b/c/../../../d/e/f"), "d\\e\\f"); + + info("Forwardslash-separated, with a drive"); + Assert.equal(Win.normalize("c:/"), "c:\\"); + Assert.equal(Win.normalize("c:/a/b/c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:/a/b////c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:////a/b/c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:/a/b/c///"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:/a/b/c/../../../d/e/f"), "c:\\d\\e\\f"); + Assert.equal(Win.normalize("c:a/b/c/../../../d/e/f"), "c:\\d\\e\\f"); + + info("Backslash-separated, UNC-style"); + Assert.equal(Win.basename("\\\\a\\b"), "b"); + Assert.equal(Win.basename("\\\\a\\b\\"), ""); + Assert.equal(Win.basename("\\\\abc"), ""); + Assert.equal(Win.dirname("\\\\a\\b"), "\\\\a"); + Assert.equal(Win.dirname("\\\\a\\b\\"), "\\\\a\\b"); + Assert.equal(Win.dirname("\\\\a\\\\\\\\b"), "\\\\a"); + Assert.equal(Win.dirname("\\\\abc"), "\\\\abc"); + Assert.equal(Win.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c"); + Assert.equal(Win.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c"); + Assert.equal(Win.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c"); + Assert.equal(Win.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f"); + do_check_fail(() => Win.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f")); + + Assert.equal(Win.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar"); + Assert.equal(Win.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar"); + Assert.equal(Win.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar"); + Assert.equal(Win.winGetDrive("\\\\"), null); + Assert.equal(Win.winGetDrive("\\\\c"), "\\\\c"); + Assert.equal(Win.winGetDrive("\\\\c\\abc"), "\\\\c"); + + info("Testing unix paths"); + Assert.equal(Unix.basename("a/b"), "b"); + Assert.equal(Unix.basename("a/b/"), ""); + Assert.equal(Unix.basename("abc"), "abc"); + Assert.equal(Unix.dirname("a/b"), "a"); + Assert.equal(Unix.dirname("a/b/"), "a/b"); + Assert.equal(Unix.dirname("a////b"), "a"); + Assert.equal(Unix.dirname("abc"), "."); + Assert.equal(Unix.normalize("/a/b/c"), "/a/b/c"); + Assert.equal(Unix.normalize("/a/b////c"), "/a/b/c"); + Assert.equal(Unix.normalize("////a/b/c"), "/a/b/c"); + Assert.equal(Unix.normalize("/a/b/c///"), "/a/b/c"); + Assert.equal(Unix.normalize("/a/b/c/../../../d/e/f"), "/d/e/f"); + Assert.equal(Unix.normalize("a/b/c/../../../d/e/f"), "d/e/f"); + do_check_fail(() => Unix.normalize("/a/b/c/../../../../d/e/f")); + + Assert.equal( + Unix.join("/tmp", "foo", "bar"), + "/tmp/foo/bar", + "join /tmp,foo,bar" + ); + Assert.equal( + Unix.join("/tmp", "/foo", "bar"), + "/foo/bar", + "join /tmp,/foo,bar" + ); + + info("Testing the presence of ospath.jsm"); + let scope; + try { + scope = ChromeUtils.import("resource://gre/modules/osfile/ospath.jsm"); + } catch (ex) { + // Can't load ospath + } + Assert.ok(!!scope.basename); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_path_constants.js b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js new file mode 100644 index 0000000000..3b24b62761 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); +const { makeFakeAppDir } = ChromeUtils.importESModule( + "resource://testing-common/AppData.sys.mjs" +); + +function compare_paths(ospath, key) { + let file; + try { + file = Services.dirsvc.get(key, Ci.nsIFile); + } catch (ex) {} + + if (file) { + Assert.ok(!!ospath); + Assert.equal(ospath, file.path); + } else { + info( + "WARNING: " + key + " is not defined. Test may not be testing anything!" + ); + Assert.ok(!ospath); + } +} + +// Test simple paths +add_task(async function test_simple_paths() { + Assert.ok(!!OS.Constants.Path.tmpDir); + compare_paths(OS.Constants.Path.tmpDir, "TmpD"); +}); + +// Some path constants aren't set up until the profile is available. This +// test verifies that behavior. +add_task(async function test_before_after_profile() { + // On Android the profile is initialized during xpcshell init, so this test + // will fail. + if (AppConstants.platform != "android") { + Assert.equal(null, OS.Constants.Path.profileDir); + Assert.equal(null, OS.Constants.Path.localProfileDir); + Assert.equal(null, OS.Constants.Path.userApplicationDataDir); + } + + do_get_profile(); + Assert.ok(!!OS.Constants.Path.profileDir); + Assert.ok(!!OS.Constants.Path.localProfileDir); + + // UAppData is still null because the xpcshell profile doesn't set it up. + // This test is mostly here to fail in case behavior of do_get_profile() ever + // changes. We want to know if our assumptions no longer hold! + Assert.equal(null, OS.Constants.Path.userApplicationDataDir); + + await makeFakeAppDir(); + Assert.ok(!!OS.Constants.Path.userApplicationDataDir); + + // FUTURE: verify AppData too (bug 964291). +}); + +// Test presence of paths that only exist on Desktop platforms +add_task(async function test_desktop_paths() { + if (OS.Constants.Sys.Name == "Android") { + return; + } + Assert.ok(!!OS.Constants.Path.homeDir); + + compare_paths(OS.Constants.Path.homeDir, "Home"); + compare_paths(OS.Constants.Path.userApplicationDataDir, "UAppData"); + + compare_paths(OS.Constants.Path.macUserLibDir, "ULibDir"); +}); + +// Open libxul +add_task(async function test_libxul() { + ctypes.open(OS.Constants.Path.libxul); + info("Linked to libxul"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_queue.js b/toolkit/components/osfile/tests/xpcshell/test_queue.js new file mode 100644 index 0000000000..e6e6f841c3 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_queue.js @@ -0,0 +1,34 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// Check if Scheduler.queue returned by OS.File.queue is resolved initially. +add_task(async function check_init() { + await OS.File.queue; + info("Function resolved"); +}); + +// Check if Scheduler.queue returned by OS.File.queue is resolved +// after an operation is successful. +add_task(async function check_success() { + info("Attempting to open a file correctly"); + await OS.File.open(OS.Path.join(do_get_cwd().path, "test_queue.js")); + info("File opened correctly"); + await OS.File.queue; + info("Function resolved"); +}); + +// Check if Scheduler.queue returned by OS.File.queue is resolved +// after an operation fails. +add_task(async function check_failure() { + let exception; + try { + info("Attempting to open a non existing file"); + await OS.File.open(OS.Path.join(".", "Bigfoot")); + } catch (err) { + exception = err; + await OS.File.queue; + } + Assert.ok(exception != null); + info("Function resolved"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_read_write.js b/toolkit/components/osfile/tests/xpcshell/test_read_write.js new file mode 100644 index 0000000000..6fe554c922 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_read_write.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var SHARED_PATH; + +var EXISTING_FILE = do_get_file("xpcshell.ini").path; + +add_task(async function init() { + do_get_profile(); + SHARED_PATH = OS.Path.join( + OS.Constants.Path.profileDir, + "test_osfile_read.tmp" + ); +}); + +// Check that OS.File.read() is executed after the previous operation +add_test_pair(async function ordering() { + let string1 = "Initial state " + Math.random(); + let string2 = "After writing " + Math.random(); + await OS.File.writeAtomic(SHARED_PATH, string1); + OS.File.writeAtomic(SHARED_PATH, string2); + let string3 = await OS.File.read(SHARED_PATH, { encoding: "utf-8" }); + Assert.equal(string3, string2); +}); + +add_test_pair(async function read_write_all() { + let DEST_PATH = SHARED_PATH + Math.random(); + let TMP_PATH = DEST_PATH + ".tmp"; + + let test_with_options = function(options, suffix) { + return (async function() { + info( + "Running test read_write_all with options " + JSON.stringify(options) + ); + let TEST = "read_write_all " + suffix; + + let optionsBackup = JSON.parse(JSON.stringify(options)); + + // Check that read + writeAtomic performs a correct copy + let currentDir = await OS.File.getCurrentDirectory(); + let pathSource = OS.Path.join(currentDir, EXISTING_FILE); + let contents = await OS.File.read(pathSource); + Assert.ok(!!contents); // Content is not empty + let bytesRead = contents.byteLength; + + let bytesWritten = await OS.File.writeAtomic( + DEST_PATH, + contents, + options + ); + Assert.equal(bytesRead, bytesWritten); // Correct number of bytes written + + // Check that options are not altered + Assert.equal(JSON.stringify(options), JSON.stringify(optionsBackup)); + await reference_compare_files(pathSource, DEST_PATH, TEST); + + // Check that temporary file was removed or never created exist + Assert.ok(!new FileUtils.File(TMP_PATH).exists()); + + // Check that writeAtomic fails if noOverwrite is true and the destination + // file already exists! + contents = new Uint8Array(300); + let view = new Uint8Array(contents.buffer, 10, 200); + try { + let opt = JSON.parse(JSON.stringify(options)); + opt.noOverwrite = true; + await OS.File.writeAtomic(DEST_PATH, view, opt); + do_throw( + "With noOverwrite, writeAtomic should have refused to overwrite file (" + + suffix + + ")" + ); + } catch (err) { + if (err instanceof OS.File.Error && err.becauseExists) { + info( + "With noOverwrite, writeAtomic correctly failed (" + suffix + ")" + ); + } else { + throw err; + } + } + await reference_compare_files(pathSource, DEST_PATH, TEST); + + // Check that temporary file was removed or never created + Assert.ok(!new FileUtils.File(TMP_PATH).exists()); + + // Now write a subset + let START = 10; + let LENGTH = 100; + contents = new Uint8Array(300); + for (let i = 0; i < contents.byteLength; i++) { + contents[i] = i % 256; + } + view = new Uint8Array(contents.buffer, START, LENGTH); + bytesWritten = await OS.File.writeAtomic(DEST_PATH, view, options); + Assert.equal(bytesWritten, LENGTH); + + let array2 = await OS.File.read(DEST_PATH); + Assert.equal(LENGTH, array2.length); + for (let j = 0; j < LENGTH; j++) { + Assert.equal(array2[j], (j + START) % 256); + } + + // Cleanup. + await OS.File.remove(DEST_PATH); + await OS.File.remove(TMP_PATH); + })(); + }; + + await test_with_options({ tmpPath: TMP_PATH }, "Renaming, not flushing"); + await test_with_options( + { tmpPath: TMP_PATH, flush: true }, + "Renaming, flushing" + ); + await test_with_options({}, "Not renaming, not flushing"); + await test_with_options({ flush: true }, "Not renaming, flushing"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_remove.js b/toolkit/components/osfile/tests/xpcshell/test_remove.js new file mode 100644 index 0000000000..f638d99000 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_remove.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +function run_test() { + Services.prefs.setBoolPref("toolkit.osfile.log", true); + run_next_test(); +} + +add_task(async function test_ignoreAbsent() { + let absent_file_name = "test_osfile_front_absent.tmp"; + + // Removing absent files should throw if "ignoreAbsent" is true. + await Assert.rejects( + OS.File.remove(absent_file_name, { ignoreAbsent: false }), + err => err.operation == "remove", + "OS.File.remove throws if there is no such file." + ); + + // Removing absent files should not throw if "ignoreAbsent" is true or not + // defined. + let exception = null; + try { + await OS.File.remove(absent_file_name, { ignoreAbsent: true }); + await OS.File.remove(absent_file_name); + } catch (ex) { + exception = ex; + } + Assert.ok(!exception, "OS.File.remove should not throw when not requested."); +}); + +add_task(async function test_ignoreAbsent_directory_missing() { + let absent_file_name = OS.Path.join("absent_parent", "test.tmp"); + + // Removing absent files should throw if "ignoreAbsent" is true. + await Assert.rejects( + OS.File.remove(absent_file_name, { ignoreAbsent: false }), + err => err.operation == "remove", + "OS.File.remove throws if there is no such file." + ); + + // Removing files from absent directories should not throw if "ignoreAbsent" + // is true or not defined. + let exception = null; + try { + await OS.File.remove(absent_file_name, { ignoreAbsent: true }); + await OS.File.remove(absent_file_name); + } catch (ex) { + exception = ex; + } + Assert.ok(!exception, "OS.File.remove should not throw when not requested."); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js new file mode 100644 index 0000000000..a246afa86f --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +function run_test() { + Services.prefs.setBoolPref("toolkit.osfile.log", true); + + run_next_test(); +} + +add_task(async function() { + // Set up profile. We create the directory in the profile, because the profile + // is removed after every test run. + do_get_profile(); + + let file = OS.Path.join(OS.Constants.Path.profileDir, "file"); + let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory"); + let file1 = OS.Path.join(dir, "file1"); + let file2 = OS.Path.join(dir, "file2"); + let subDir = OS.Path.join(dir, "subdir"); + let fileInSubDir = OS.Path.join(subDir, "file"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Remove non-existent directory + let exception = null; + try { + await OS.File.removeDir(dir, { ignoreAbsent: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + + // Remove non-existent directory with ignoreAbsent + await OS.File.removeDir(dir, { ignoreAbsent: true }); + await OS.File.removeDir(dir); + + // Remove file with ignoreAbsent: false + await OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" }); + exception = null; + try { + await OS.File.removeDir(file, { ignoreAbsent: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + + // Remove empty directory + await OS.File.makeDir(dir); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + + // Remove directory that contains one file + await OS.File.makeDir(dir); + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + + // Remove directory that contains multiple files + await OS.File.makeDir(dir); + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + await OS.File.writeAtomic(file2, "content", { tmpPath: file2 + ".tmp" }); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + + // Remove directory that contains a file and a directory + await OS.File.makeDir(dir); + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + await OS.File.makeDir(subDir); + await OS.File.writeAtomic(fileInSubDir, "content", { + tmpPath: fileInSubDir + ".tmp", + }); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); +}); + +add_task(async function test_unix_symlink() { + // Windows does not implement OS.File.unixSymLink() + if (OS.Constants.Win) { + return; + } + + // Android / B2G file systems typically don't support symlinks. + if (OS.Constants.Sys.Name == "Android") { + return; + } + + let file = OS.Path.join(OS.Constants.Path.profileDir, "file"); + let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory"); + let file1 = OS.Path.join(dir, "file1"); + + // This test will create the following directory structure: + // <profileDir>/file (regular file) + // <profileDir>/file.link => file (symlink) + // <profileDir>/directory (directory) + // <profileDir>/linkdir => directory (directory) + // <profileDir>/directory/file1 (regular file) + // <profileDir>/directory3 (directory) + // <profileDir>/directory3/file3 (directory) + // <profileDir>/directory/link2 => ../directory3 (regular file) + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + await OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" }); + Assert.ok(await OS.File.exists(file)); + let info = await OS.File.stat(file, { unixNoFollowingLinks: true }); + Assert.ok(!info.isDir); + Assert.ok(!info.isSymLink); + + await OS.File.unixSymLink(file, file + ".link"); + Assert.ok(await OS.File.exists(file + ".link")); + info = await OS.File.stat(file + ".link", { unixNoFollowingLinks: true }); + Assert.ok(!info.isDir); + Assert.ok(info.isSymLink); + info = await OS.File.stat(file + ".link"); + Assert.ok(!info.isDir); + Assert.ok(!info.isSymLink); + await OS.File.remove(file + ".link"); + Assert.equal(false, await OS.File.exists(file + ".link")); + + await OS.File.makeDir(dir); + Assert.ok(await OS.File.exists(dir)); + info = await OS.File.stat(dir, { unixNoFollowingLinks: true }); + Assert.ok(info.isDir); + Assert.ok(!info.isSymLink); + + let link = OS.Path.join(OS.Constants.Path.profileDir, "linkdir"); + + await OS.File.unixSymLink(dir, link); + Assert.ok(await OS.File.exists(link)); + info = await OS.File.stat(link, { unixNoFollowingLinks: true }); + Assert.ok(!info.isDir); + Assert.ok(info.isSymLink); + info = await OS.File.stat(link); + Assert.ok(info.isDir); + Assert.ok(!info.isSymLink); + + let dir3 = OS.Path.join(OS.Constants.Path.profileDir, "directory3"); + let file3 = OS.Path.join(dir3, "file3"); + let link2 = OS.Path.join(dir, "link2"); + + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + Assert.ok(await OS.File.exists(file1)); + await OS.File.makeDir(dir3); + Assert.ok(await OS.File.exists(dir3)); + await OS.File.writeAtomic(file3, "content", { tmpPath: file3 + ".tmp" }); + Assert.ok(await OS.File.exists(file3)); + await OS.File.unixSymLink("../directory3", link2); + Assert.ok(await OS.File.exists(link2)); + + await OS.File.removeDir(link); + Assert.equal(false, await OS.File.exists(link)); + Assert.ok(await OS.File.exists(file1)); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + Assert.ok(await OS.File.exists(file3)); + await OS.File.removeDir(dir3); + Assert.equal(false, await OS.File.exists(dir3)); + + // This task will be executed only on Unix-like systems. + // Please do not add tests independent to operating systems here + // or implement symlink() on Windows. +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js new file mode 100644 index 0000000000..a81463bc7f --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +function run_test() { + Services.prefs.setBoolPref("toolkit.osfile.log", true); + + run_next_test(); +} + +/** + * Test OS.File.removeEmptyDir + */ +add_task(async function() { + // Set up profile. We create the directory in the profile, because the profile + // is removed after every test run. + do_get_profile(); + + let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Remove non-existent directory + await OS.File.removeEmptyDir(dir); + + // Remove non-existent directory with ignoreAbsent + await OS.File.removeEmptyDir(dir, { ignoreAbsent: true }); + + // Remove non-existent directory with ignoreAbsent false + let exception = null; + try { + await OS.File.removeEmptyDir(dir, { ignoreAbsent: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseNoSuchFile); + + // Remove empty directory + await OS.File.makeDir(dir); + await OS.File.removeEmptyDir(dir); + Assert.equal(false, await OS.File.exists(dir)); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_reset.js b/toolkit/components/osfile/tests/xpcshell/test_reset.js new file mode 100644 index 0000000000..41dea1e9dd --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_reset.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var Path = OS.Constants.Path; + +add_task(async function init() { + do_get_profile(); +}); + +add_task(async function reset_before_launching() { + info("Reset without launching OS.File, it shouldn't break"); + await OS.File.resetWorker(); +}); + +add_task(async function transparent_reset() { + for (let i = 1; i < 3; ++i) { + info( + "Do stome stuff before and after " + + i + + " reset(s), " + + "it shouldn't break" + ); + let CONTENT = "some content " + i; + let path = OS.Path.join(Path.profileDir, "tmp"); + await OS.File.writeAtomic(path, CONTENT); + for (let j = 0; j < i; ++j) { + await OS.File.resetWorker(); + } + let data = await OS.File.read(path); + let string = new TextDecoder().decode(data); + Assert.equal(string, CONTENT); + } +}); + +add_task(async function file_open_cannot_reset() { + let TEST_FILE = OS.Path.join(Path.profileDir, "tmp-" + Math.random()); + info( + "Leaking file descriptor " + TEST_FILE + ", we shouldn't be able to reset" + ); + let openedFile = await OS.File.open(TEST_FILE, { create: true }); + let thrown = false; + try { + await OS.File.resetWorker(); + } catch (ex) { + if (ex.message.includes(OS.Path.basename(TEST_FILE))) { + thrown = true; + } else { + throw ex; + } + } + Assert.ok(thrown); + + info("Closing the file, we should now be able to reset"); + await openedFile.close(); + await OS.File.resetWorker(); +}); + +add_task(async function dir_open_cannot_reset() { + let TEST_DIR = await OS.File.getCurrentDirectory(); + info("Leaking directory " + TEST_DIR + ", we shouldn't be able to reset"); + let iterator = new OS.File.DirectoryIterator(TEST_DIR); + let thrown = false; + try { + await OS.File.resetWorker(); + } catch (ex) { + if (ex.message.includes(OS.Path.basename(TEST_DIR))) { + thrown = true; + } else { + throw ex; + } + } + Assert.ok(thrown); + + info("Closing the directory, we should now be able to reset"); + await iterator.close(); + await OS.File.resetWorker(); +}); + +add_task(async function race_against_itself() { + info("Attempt to get resetWorker() to race against itself"); + // Arbitrary operation, just to wake up the worker + try { + await OS.File.read("/foo"); + } catch (ex) {} + + let all = []; + for (let i = 0; i < 100; ++i) { + all.push(OS.File.resetWorker()); + } + + await Promise.all(all); +}); + +add_task(async function finish_with_a_reset() { + info("Reset without waiting for the result"); + // Arbitrary operation, just to wake up the worker + try { + await OS.File.read("/foo"); + } catch (ex) {} + // Now reset + /* don't yield*/ OS.File.resetWorker(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_shutdown.js b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js new file mode 100644 index 0000000000..9e8c696481 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js @@ -0,0 +1,103 @@ +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +add_task(function init() { + do_get_profile(); +}); + +/** + * Test logging of file descriptors leaks. + */ +add_task(async function system_shutdown() { + // Test that unclosed files cause warnings + // Test that unclosed directories cause warnings + // Test that closed files do not cause warnings + // Test that closed directories do not cause warnings + function testLeaksOf(resource, topic) { + return (async function() { + let deferred = PromiseUtils.defer(); + + // Register observer + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + Services.prefs.setBoolPref("toolkit.osfile.log", true); + Services.prefs.setBoolPref("toolkit.osfile.log.redirect", true); + Services.prefs.setCharPref( + "toolkit.osfile.test.shutdown.observer", + topic + ); + + let observer = function(aMessage) { + try { + info("Got message: " + aMessage); + if (!(aMessage instanceof Ci.nsIConsoleMessage)) { + return; + } + let message = aMessage.message; + info("Got message: " + message); + if (!message.includes("TEST OS Controller WARNING")) { + return; + } + info( + "Got message: " + message + ", looking for resource " + resource + ); + if (!message.includes(resource)) { + return; + } + info("Resource: " + resource + " found"); + executeSoon(deferred.resolve); + } catch (ex) { + executeSoon(function() { + deferred.reject(ex); + }); + } + }; + Services.console.registerListener(observer); + Services.obs.notifyObservers(null, topic); + do_timeout(1000, function() { + info("Timeout while waiting for resource: " + resource); + deferred.reject("timeout"); + }); + + let resolved = false; + try { + await deferred.promise; + resolved = true; + } catch (ex) { + if (ex == "timeout") { + resolved = false; + } else { + throw ex; + } + } + Services.console.unregisterListener(observer); + Services.prefs.clearUserPref("toolkit.osfile.log"); + Services.prefs.clearUserPref("toolkit.osfile.log.redirect"); + Services.prefs.clearUserPref("toolkit.osfile.test.shutdown.observer"); + Services.prefs.clearUserPref("toolkit.async_shutdown.testing"); + + return resolved; + })(); + } + + let TEST_DIR = OS.Path.join(await OS.File.getCurrentDirectory(), ".."); + info("Testing for leaks of directory iterator " + TEST_DIR); + let iterator = new OS.File.DirectoryIterator(TEST_DIR); + info("At this stage, we leak the directory"); + Assert.ok(await testLeaksOf(TEST_DIR, "test.shutdown.dir.leak")); + await iterator.close(); + info("At this stage, we don't leak the directory anymore"); + Assert.equal(false, await testLeaksOf(TEST_DIR, "test.shutdown.dir.noleak")); + + let TEST_FILE = OS.Path.join(OS.Constants.Path.profileDir, "test"); + info("Testing for leaks of file descriptor: " + TEST_FILE); + let openedFile = await OS.File.open(TEST_FILE, { create: true }); + info("At this stage, we leak the file"); + Assert.ok(await testLeaksOf(TEST_FILE, "test.shutdown.file.leak")); + await openedFile.close(); + info("At this stage, we don't leak the file anymore"); + Assert.equal( + false, + await testLeaksOf(TEST_FILE, "test.shutdown.file.leak.2") + ); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_telemetry.js b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js new file mode 100644 index 0000000000..178a27b3d8 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js @@ -0,0 +1,61 @@ +"use strict"; + +var { + OS: { File, Path, Constants }, +} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// Ensure that we have a profile but that the OS.File worker is not launched +add_task(async function init() { + do_get_profile(); + await File.resetWorker(); +}); + +function getCount(histogram) { + if (histogram == null) { + return 0; + } + + let total = 0; + for (let i of Object.values(histogram.values)) { + total += i; + } + return total; +} + +// Ensure that launching the OS.File worker adds data to the relevant +// histograms +add_task(async function test_startup() { + let LAUNCH = "OSFILE_WORKER_LAUNCH_MS"; + let READY = "OSFILE_WORKER_READY_MS"; + + let before = Services.telemetry.getSnapshotForHistograms("main", false) + .parent; + + // Launch the OS.File worker + await File.getCurrentDirectory(); + + let after = Services.telemetry.getSnapshotForHistograms("main", false).parent; + + info("Ensuring that we have recorded measures for histograms"); + Assert.equal(getCount(after[LAUNCH]), getCount(before[LAUNCH]) + 1); + Assert.equal(getCount(after[READY]), getCount(before[READY]) + 1); + + info("Ensuring that launh <= ready"); + Assert.ok(after[LAUNCH].sum <= after[READY].sum); +}); + +// Ensure that calling writeAtomic adds data to the relevant histograms +add_task(async function test_writeAtomic() { + let LABEL = "OSFILE_WRITEATOMIC_JANK_MS"; + + let before = Services.telemetry.getSnapshotForHistograms("main", false) + .parent; + + // Perform a write. + let path = Path.join(Constants.Path.profileDir, "test_osfile_telemetry.tmp"); + await File.writeAtomic(path, LABEL, { tmpPath: path + ".tmp" }); + + let after = Services.telemetry.getSnapshotForHistograms("main", false).parent; + + Assert.equal(getCount(after[LABEL]), getCount(before[LABEL]) + 1); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_unique.js b/toolkit/components/osfile/tests/xpcshell/test_unique.js new file mode 100644 index 0000000000..740d84a2d6 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_unique.js @@ -0,0 +1,87 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_get_profile(); + run_next_test(); +} + +function testFiles(filename) { + return (async function() { + const MAX_TRIES = 10; + let profileDir = OS.Constants.Path.profileDir; + let path = OS.Path.join(profileDir, filename); + + // Ensure that openUnique() uses the file name if there is no file with that name already. + let openedFile = await OS.File.openUnique(path); + info("\nCreate new file: " + openedFile.path); + await openedFile.file.close(); + let exists = await OS.File.exists(openedFile.path); + Assert.ok(exists); + Assert.equal(path, openedFile.path); + let fileInfo = await OS.File.stat(openedFile.path); + Assert.ok(fileInfo.size == 0); + + // Ensure that openUnique() creates a new file name using a HEX number, as the original name is already taken. + openedFile = await OS.File.openUnique(path); + info("\nCreate unique HEX file: " + openedFile.path); + await openedFile.file.close(); + exists = await OS.File.exists(openedFile.path); + Assert.ok(exists); + fileInfo = await OS.File.stat(openedFile.path); + Assert.ok(fileInfo.size == 0); + + // Ensure that openUnique() generates different file names each time, using the HEX number algorithm + let filenames = new Set(); + for (let i = 0; i < MAX_TRIES; i++) { + openedFile = await OS.File.openUnique(path); + await openedFile.file.close(); + filenames.add(openedFile.path); + } + + Assert.equal(filenames.size, MAX_TRIES); + + // Ensure that openUnique() creates a new human readable file name using, as the original name is already taken. + openedFile = await OS.File.openUnique(path, { humanReadable: true }); + info("\nCreate unique Human Readable file: " + openedFile.path); + await openedFile.file.close(); + exists = await OS.File.exists(openedFile.path); + Assert.ok(exists); + fileInfo = await OS.File.stat(openedFile.path); + Assert.ok(fileInfo.size == 0); + + // Ensure that openUnique() generates different human readable file names each time + filenames = new Set(); + for (let i = 0; i < MAX_TRIES; i++) { + openedFile = await OS.File.openUnique(path, { humanReadable: true }); + await openedFile.file.close(); + filenames.add(openedFile.path); + } + + Assert.equal(filenames.size, MAX_TRIES); + + let exn; + try { + for (let i = 0; i < 100; i++) { + openedFile = await OS.File.openUnique(path, { humanReadable: true }); + await openedFile.file.close(); + } + } catch (ex) { + exn = ex; + } + + info("Ensure that this raises the correct error"); + Assert.ok(!!exn); + Assert.ok(exn instanceof OS.File.Error); + Assert.ok(exn.becauseExists); + })(); +} + +add_task(async function test_unique() { + OS.Shared.DEBUG = true; + // Tests files with extension + await testFiles("dummy_unique_file.txt"); + // Tests files with no extension + await testFiles("dummy_unique_file_no_ext"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..02b7345a3c --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini @@ -0,0 +1,48 @@ +[DEFAULT] +head = head.js + +[test_compression.js] +[test_constants.js] +[test_duration.js] +[test_exception.js] +[test_file_URL_conversion.js] +[test_logging.js] +[test_makeDir.js] +[test_open.js] +[test_osfile_async.js] +[test_osfile_async_append.js] +[test_osfile_async_bytes.js] +[test_osfile_async_copy.js] +[test_osfile_async_flush.js] +[test_osfile_async_largefiles.js] +[test_osfile_async_setDates.js] +# Unimplemented on Windows (bug 1022816). +# Spurious failure on Android test farm due to non-POSIX behavior of +# filesystem backing /mnt/sdcard (not worth trying to fix). +[test_osfile_async_setPermissions.js] +skip-if = os == "win" || os == "android" +[test_osfile_closed.js] +[test_osfile_error.js] +[test_osfile_kill.js] +# Windows test +[test_osfile_win_async_setPermissions.js] +skip-if = os != "win" +[test_osfile_writeAtomic_backupTo_option.js] +[test_osfile_writeAtomic_zerobytes.js] +[test_osfile_writeAtomic_unicode_filename.js] +[test_path.js] +[test_path_constants.js] +[test_queue.js] +[test_read_write.js] +requesttimeoutfactor = 4 +[test_remove.js] +[test_removeDir.js] +requesttimeoutfactor = 4 +[test_removeEmptyDir.js] +[test_reset.js] +[test_shutdown.js] +[test_telemetry.js] +# On Android, we use OS.File during xpcshell initialization, so the expected +# telemetry cannot be observed. +skip-if = toolkit == "android" +[test_unique.js] |