summaryrefslogtreecommitdiffstats
path: root/toolkit/components/osfile/tests
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/osfile/tests')
-rw-r--r--toolkit/components/osfile/tests/mochi/chrome.ini15
-rw-r--r--toolkit/components/osfile/tests/mochi/main_test_osfile_async.js501
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_async.xhtml21
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_back.xhtml44
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_comms.xhtml88
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_front.xhtml42
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js205
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js696
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js257
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js310
-rw-r--r--toolkit/components/osfile/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/osfile/tests/xpcshell/head.js109
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_compression.js106
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_constants.js20
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_duration.js127
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_exception.js108
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js119
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_logging.js73
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_makeDir.js137
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_open.js75
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async.js13
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js105
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js40
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js109
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js31
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js135
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js214
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js102
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js46
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_error.js56
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js97
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js135
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js148
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_unicode_filename.js48
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js26
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_path.js187
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_path_constants.js81
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_queue.js34
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_read_write.js119
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_remove.js60
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_removeDir.js177
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js54
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_reset.js102
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_shutdown.js103
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_telemetry.js61
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_unique.js87
-rw-r--r--toolkit/components/osfile/tests/xpcshell/xpcshell.ini48
47 files changed, 5478 insertions, 0 deletions
diff --git a/toolkit/components/osfile/tests/mochi/chrome.ini b/toolkit/components/osfile/tests/mochi/chrome.ini
new file mode 100644
index 0000000000..c36cf2c045
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/chrome.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ main_test_osfile_async.js
+ worker_test_osfile_comms.js
+ worker_test_osfile_front.js
+ worker_test_osfile_unix.js
+ worker_test_osfile_win.js
+
+[test_osfile_async.xhtml]
+[test_osfile_back.xhtml]
+[test_osfile_comms.xhtml]
+[test_osfile_front.xhtml]
+skip-if =
+ win11_2009 && bits == 32 # Bug 1809355
diff --git a/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
new file mode 100644
index 0000000000..0a1fe938d4
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
@@ -0,0 +1,501 @@
+"use strict";
+
+const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+// The following are used to compare against a well-tested reference
+// implementation of file I/O.
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+var myok = ok;
+var myis = is;
+var myinfo = info;
+var myisnot = isnot;
+
+var isPromise = function ispromise(value) {
+ return value != null && typeof value == "object" && "then" in value;
+};
+
+var maketest = function(prefix, test) {
+ let utils = {
+ ok: function ok(t, m) {
+ myok(t, prefix + ": " + m);
+ },
+ is: function is(l, r, m) {
+ myis(l, r, prefix + ": " + m);
+ },
+ isnot: function isnot(l, r, m) {
+ myisnot(l, r, prefix + ": " + m);
+ },
+ info: function info(m) {
+ myinfo(prefix + ": " + m);
+ },
+ fail: function fail(m) {
+ utils.ok(false, m);
+ },
+ okpromise: function okpromise(t, m) {
+ return t.then(
+ function onSuccess() {
+ utils.ok(true, m);
+ },
+ function onFailure() {
+ utils.ok(false, m);
+ }
+ );
+ },
+ };
+ return function runtest() {
+ utils.info("Entering");
+ try {
+ let result = test.call(this, utils);
+ if (!isPromise(result)) {
+ throw new TypeError("The test did not return a promise");
+ }
+ utils.info("This was a promise");
+ // The test returns a promise
+ result = result.then(
+ function test_complete() {
+ utils.info("Complete");
+ },
+ function catch_uncaught_errors(err) {
+ utils.fail("Uncaught error " + err);
+ if (err && typeof err == "object" && "message" in err) {
+ utils.fail("(" + err.message + ")");
+ }
+ if (err && typeof err == "object" && "stack" in err) {
+ utils.fail("at " + err.stack);
+ }
+ }
+ );
+ return result;
+ } catch (x) {
+ utils.fail("Error " + x + " at " + x.stack);
+ return null;
+ }
+ };
+};
+
+/**
+ * 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, test) {
+ 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);
+ }
+ }
+ );
+ });
+};
+
+var reference_dir_contents = function reference_dir_contents(path) {
+ let result = [];
+ let entries = new FileUtils.File(path).directoryEntries;
+ while (entries.hasMoreElements()) {
+ let entry = entries.nextFile;
+ result.push(entry.path);
+ }
+ return result;
+};
+
+// Set/Unset OS.Shared.DEBUG, OS.Shared.TEST and a console listener.
+function toggleDebugTest(pref, consoleListener) {
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref);
+ Services.console[pref ? "registerListener" : "unregisterListener"](
+ consoleListener
+ );
+}
+
+var test = maketest("Main", function main(test) {
+ return (async function() {
+ SimpleTest.waitForExplicitFinish();
+ await test_stat();
+ await test_debug();
+ await test_info_features_detect();
+ await test_position();
+ await test_iter();
+ await test_exists();
+ await test_debug_test();
+ info("Test is over");
+ SimpleTest.finish();
+ })();
+});
+
+/**
+ * A file that we know exists and that can be used for reading.
+ */
+var EXISTING_FILE = OS.Path.join(
+ "chrome",
+ "toolkit",
+ "components",
+ "osfile",
+ "tests",
+ "mochi",
+ "main_test_osfile_async.js"
+);
+
+/**
+ * Test OS.File.stat and OS.File.prototype.stat
+ */
+var test_stat = maketest("stat", function stat(test) {
+ return (async function() {
+ // Open a file and stat it
+ let file = await OS.File.open(EXISTING_FILE);
+ let stat1;
+
+ try {
+ test.info("Stating file");
+ stat1 = await file.stat();
+ test.ok(true, "stat has worked " + stat1);
+ test.ok(stat1, "stat is not empty");
+ } finally {
+ await file.close();
+ }
+
+ // Stat the same file without opening it
+ test.info("Stating a file without opening it");
+ let stat2 = await OS.File.stat(EXISTING_FILE);
+ test.ok(true, "stat 2 has worked " + stat2);
+ test.ok(stat2, "stat 2 is not empty");
+ for (let key in stat2) {
+ test.is(
+ "" + stat1[key],
+ "" + stat2[key],
+ "Stat field " + key + "is the same"
+ );
+ }
+ })();
+});
+
+/**
+ * Test feature detection using OS.File.Info.prototype on main thread
+ */
+var test_info_features_detect = maketest(
+ "features_detect",
+ function features_detect(test) {
+ return (async function() {
+ if (!OS.Constants.Win && OS.Constants.libc) {
+ // see if unixGroup is defined
+ if ("unixGroup" in OS.File.Info.prototype) {
+ test.ok(true, "unixGroup is defined");
+ } else {
+ test.fail("unixGroup is not defined though we are under Unix");
+ }
+ }
+ })();
+ }
+);
+
+/**
+ * Test file.{getPosition, setPosition}
+ */
+var test_position = maketest("position", function position(test) {
+ return (async function() {
+ let file = await OS.File.open(EXISTING_FILE);
+
+ try {
+ let view = await file.read();
+ test.info("First batch of content read");
+ let CHUNK_SIZE = 178; // An arbitrary number of bytes to read from the file
+ let pos = await file.getPosition();
+ test.info("Obtained position");
+ test.is(pos, view.byteLength, "getPosition returned the end of the file");
+ pos = await file.setPosition(-CHUNK_SIZE, OS.File.POS_END);
+ test.info("Changed position");
+ test.is(
+ pos,
+ view.byteLength - CHUNK_SIZE,
+ "setPosition returned the correct position"
+ );
+
+ let view2 = await file.read();
+ test.info("Read the end of the file");
+ for (let i = 0; i < CHUNK_SIZE; ++i) {
+ if (view2[i] != view[i + view.byteLength - CHUNK_SIZE]) {
+ test.is(
+ view2[i],
+ view[i],
+ "setPosition put us in the right position"
+ );
+ }
+ }
+ } finally {
+ await file.close();
+ }
+ })();
+});
+
+/**
+ * Test OS.File.prototype.{DirectoryIterator}
+ */
+var test_iter = maketest("iter", function iter(test) {
+ return (async function() {
+ let currentDir = await OS.File.getCurrentDirectory();
+
+ // Trivial walks through the directory
+ test.info("Preparing iteration");
+ let iterator = new OS.File.DirectoryIterator(currentDir);
+ let temporary_file_name = OS.Path.join(
+ currentDir,
+ "empty-temporary-file.tmp"
+ );
+ try {
+ await OS.File.remove(temporary_file_name);
+ } catch (err) {
+ // Ignore errors removing file
+ }
+ let allFiles1 = await iterator.nextBatch();
+ test.info("Obtained all files through nextBatch");
+ test.isnot(allFiles1.length, 0, "There is at least one file");
+ test.isnot(allFiles1[0].path, null, "Files have a path");
+
+ // Ensure that we have the same entries with |reference_dir_contents|
+ let referenceEntries = new Set();
+ for (let entry of reference_dir_contents(currentDir)) {
+ referenceEntries.add(entry);
+ }
+ test.is(
+ referenceEntries.size,
+ allFiles1.length,
+ "All the entries in the directory have been listed"
+ );
+ for (let entry of allFiles1) {
+ test.ok(
+ referenceEntries.has(entry.path),
+ "File " + entry.path + " effectively exists"
+ );
+ // Ensure that we have correct isDir and isSymLink
+ // Current directory is {objdir}/_tests/testing/mochitest/, assume it has some dirs and symlinks.
+ var f = new FileUtils.File(entry.path);
+ test.is(
+ entry.isDir,
+ f.isDirectory(),
+ "Get file " + entry.path + " isDir correctly"
+ );
+ test.is(
+ entry.isSymLink,
+ f.isSymlink(),
+ "Get file " + entry.path + " isSymLink correctly"
+ );
+ }
+
+ await iterator.close();
+ test.info("Closed iterator");
+
+ test.info("Double closing DirectoryIterator");
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ await iterator.close();
+ await iterator.close(); // double closing |DirectoryIterator|
+ test.ok(true, "|DirectoryIterator| was closed twice successfully");
+
+ let allFiles2 = [];
+ let i = 0;
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ await iterator.forEach(function(entry, index) {
+ test.is(i++, index, "Getting the correct index");
+ allFiles2.push(entry);
+ });
+ test.info("Obtained all files through forEach");
+ is(
+ allFiles1.length,
+ allFiles2.length,
+ "Both runs returned the same number of files"
+ );
+ for (let i = 0; i < allFiles1.length; ++i) {
+ if (allFiles1[i].path != allFiles2[i].path) {
+ test.is(
+ allFiles1[i].path,
+ allFiles2[i].path,
+ "Both runs return the same files"
+ );
+ break;
+ }
+ }
+
+ // Testing batch iteration + whether an iteration can be stopped early
+ let BATCH_LENGTH = 10;
+ test.info("Getting some files through nextBatch");
+ await iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ let someFiles1 = await iterator.nextBatch(BATCH_LENGTH);
+ let someFiles2 = await iterator.nextBatch(BATCH_LENGTH);
+ await iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ await iterator.forEach(function cb(entry, index, iterator) {
+ if (index < BATCH_LENGTH) {
+ test.is(
+ entry.path,
+ someFiles1[index].path,
+ "Both runs return the same files (part 1)"
+ );
+ } else if (index < 2 * BATCH_LENGTH) {
+ test.is(
+ entry.path,
+ someFiles2[index - BATCH_LENGTH].path,
+ "Both runs return the same files (part 2)"
+ );
+ } else if (index == 2 * BATCH_LENGTH) {
+ test.info("Attempting to stop asynchronous forEach");
+ return iterator.close();
+ } else {
+ test.fail("Can we stop an asynchronous forEach? " + index);
+ }
+ return null;
+ });
+ await iterator.close();
+
+ // Ensuring that we find new files if they appear
+ let file = await OS.File.open(temporary_file_name, { write: true });
+ file.close();
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ try {
+ let files = await iterator.nextBatch();
+ is(
+ files.length,
+ allFiles1.length + 1,
+ "The directory iterator has noticed the new file"
+ );
+ let exists = await iterator.exists();
+ test.ok(
+ exists,
+ "After nextBatch, iterator detects that the directory exists"
+ );
+ } finally {
+ await iterator.close();
+ }
+
+ // Ensuring that opening a non-existing directory fails consistently
+ // once iteration starts.
+ try {
+ iterator = null;
+ iterator = new OS.File.DirectoryIterator("/I do not exist");
+ let exists = await iterator.exists();
+ test.ok(
+ !exists,
+ "Before any iteration, iterator detects that the directory doesn't exist"
+ );
+ let exn = null;
+ try {
+ await iterator.next();
+ } catch (ex) {
+ if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ exn = ex;
+ let exists = await iterator.exists();
+ test.ok(
+ !exists,
+ "After one iteration, iterator detects that the directory doesn't exist"
+ );
+ } else {
+ throw ex;
+ }
+ }
+ test.ok(
+ exn,
+ "Iterating through a directory that does not exist has failed with becauseNoSuchFile"
+ );
+ } finally {
+ if (iterator) {
+ iterator.close();
+ }
+ }
+ test.ok(
+ !!iterator,
+ "The directory iterator for a non-existing directory was correctly created"
+ );
+ })();
+});
+
+/**
+ * Test OS.File.prototype.{exists}
+ */
+var test_exists = maketest("exists", function exists(test) {
+ return (async function() {
+ let fileExists = await OS.File.exists(EXISTING_FILE);
+ test.ok(fileExists, "file exists");
+ fileExists = await OS.File.exists(EXISTING_FILE + ".tmp");
+ test.ok(!fileExists, "file does not exists");
+ })();
+});
+
+/**
+ * Test changes to OS.Shared.DEBUG flag.
+ */
+var test_debug = maketest("debug", function debug(test) {
+ return (async function() {
+ function testSetDebugPref(pref) {
+ try {
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ } catch (x) {
+ test.fail(
+ "Setting OS.Shared.DEBUG to " + pref + " should not cause error."
+ );
+ } finally {
+ test.is(OS.Shared.DEBUG, pref, "OS.Shared.DEBUG is set correctly.");
+ }
+ }
+ testSetDebugPref(true);
+ let workerDEBUG = await OS.File.GET_DEBUG();
+ test.is(workerDEBUG, true, "Worker's DEBUG is set.");
+ testSetDebugPref(false);
+ workerDEBUG = await OS.File.GET_DEBUG();
+ test.is(workerDEBUG, false, "Worker's DEBUG is unset.");
+ })();
+});
+
+/**
+ * Test logging in the main thread with set OS.Shared.DEBUG and
+ * OS.Shared.TEST flags.
+ */
+var test_debug_test = maketest("debug_test", function debug_test(test) {
+ return (async function() {
+ // Create a console listener.
+ let consoleListener = {
+ observe(aMessage) {
+ // Ignore unexpected messages.
+ if (!(aMessage instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+ if (!aMessage.message.includes("TEST OS")) {
+ return;
+ }
+ test.ok(true, "DEBUG TEST messages are logged correctly.");
+ },
+ };
+ toggleDebugTest(true, consoleListener);
+ // Execution of OS.File.exist method will trigger OS.File.LOG several times.
+ await OS.File.exists(EXISTING_FILE);
+ toggleDebugTest(false, consoleListener);
+ })();
+});
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_async.xhtml b/toolkit/components/osfile/tests/mochi/test_osfile_async.xhtml
new file mode 100644
index 0000000000..89d8eba473
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_async.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing async I/O with OS.File"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="main_test_osfile_async.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_back.xhtml b/toolkit/components/osfile/tests/mochi/test_osfile_back.xhtml
new file mode 100644
index 0000000000..2451bd13a7
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_back.xhtml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+let worker;
+
+function test() {
+ ok(true, "test_osfile.xul: Starting test");
+ if (navigator.platform.includes("Win")) {
+ ok(true, "test_osfile.xul: Using Windows test suite");
+ worker = new ChromeWorker("worker_test_osfile_win.js");
+ } else {
+ ok(true, "test_osfile.xul: Using Unix test suite");
+ worker = new ChromeWorker("worker_test_osfile_unix.js");
+ }
+ SimpleTest.waitForExplicitFinish();
+ ok(true, "test_osfile.xul: Chrome worker created");
+ dump("MAIN: go\n");
+ listenForTests(worker);
+ worker.postMessage(0);
+ ok(true, "test_osfile.xul: Test in progress");
+};
+]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_comms.xhtml b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xhtml
new file mode 100644
index 0000000000..fd31e7bee8
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+"use strict";
+
+let worker;
+
+SpecialPowers.pushPrefEnv({"set": [["security.allow_eval_with_system_principal",
+ true]]});
+let test = function test() {
+ SimpleTest.info("test_osfile_comms.xhtml: Starting test");
+ // These are used in the worker.
+ // eslint-disable-next-line no-unused-vars
+ let { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+ // eslint-disable-next-line no-unused-vars
+ let { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ worker = new ChromeWorker("worker_test_osfile_comms.js");
+ SimpleTest.waitForExplicitFinish();
+ try {
+ worker.onerror = function onerror(error) {
+ SimpleTest.ok(false, "received error "+error);
+ }
+ worker.onmessage = function onmessage(msg) {
+ Cu.forceShrinkingGC();
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ " ("+ msg.data.a + " ==? " + msg.data.b + ")" );
+ return;
+ case "isnot":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ " ("+ msg.data.a + " !=? " + msg.data.b + ")" );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ case "value":
+ SimpleTest.ok(true, "test_osfile_comms.xhtml: Received value " + JSON.stringify(msg.data.value));
+ // eslint-disable-next-line no-eval
+ let type = eval(msg.data.typename);
+ // eslint-disable-next-line no-eval
+ let check = eval(msg.data.check);
+ let value = msg.data.value;
+ let deserialized = type.fromMsg(value);
+ check(deserialized, "Main thread test: ");
+ return;
+ default:
+ SimpleTest.ok(false, "test_osfile_comms.xhtml: wrong message "+JSON.stringify(msg.data));
+ }
+ };
+ worker.postMessage(0)
+ ok(true, "Worker launched");
+ } catch(x) {
+ // As we have set |waitForExplicitFinish|, we add this fallback
+ // in case of uncaught error, to ensure that the test does not
+ // just freeze.
+ ok(false, "Caught exception " + x + " at " + x.stack);
+ SimpleTest.finish();
+ }
+};
+
+]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_front.xhtml b/toolkit/components/osfile/tests/mochi/test_osfile_front.xhtml
new file mode 100644
index 0000000000..0a13e2d8c8
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_front.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+let worker;
+let main = this;
+
+function test() {
+ info("test_osfile_front.xhtml: Starting test");
+
+ // Test the OS.File worker
+
+ worker = new ChromeWorker("worker_test_osfile_front.js");
+ SimpleTest.waitForExplicitFinish();
+ info("test_osfile_front.xhtml: Chrome worker created");
+ dump("MAIN: go\n");
+ listenForTests(worker);
+ worker.postMessage(0);
+ ok(true, "test_osfile_front.xhtml: Test in progress");
+};
+]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js
new file mode 100644
index 0000000000..0cc1c59339
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/chrome-worker, node */
+
+"use strict";
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+
+// The set of samples for communications test. Declare as a global
+// variable to prevent this from being garbage-collected too early.
+var samples;
+
+self.onmessage = function(msg) {
+ info("Initializing");
+ self.onmessage = function on_unexpected_message(msg) {
+ throw new Error("Unexpected message " + JSON.stringify(msg.data));
+ };
+ /* import-globals-from /toolkit/components/osfile/osfile.jsm */
+ importScripts("resource://gre/modules/osfile.jsm");
+ info("Initialization complete");
+
+ samples = [
+ {
+ typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "String",
+ value: "This is a test",
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_string(candidate, prefix) {
+ is(candidate, "This is a test", prefix);
+ },
+ },
+ {
+ typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "Typed array",
+ value: (function() {
+ let view = new Uint8Array(15);
+ for (let i = 0; i < 15; ++i) {
+ view[i] = i;
+ }
+ return view;
+ })(),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_ArrayBuffer(candidate, prefix) {
+ for (let i = 0; i < 15; ++i) {
+ is(
+ candidate[i],
+ i % 256,
+ prefix +
+ "Checking that the contents of the ArrayBuffer were preserved"
+ );
+ }
+ },
+ },
+ {
+ typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "Pointer",
+ value: new OS.Shared.Type.char.in_ptr.implementation(1),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_ptr(candidate, prefix) {
+ let address = ctypes.cast(candidate, ctypes.uintptr_t).value.toString();
+ is(
+ address,
+ "1",
+ prefix + "Checking that the pointer address was preserved"
+ );
+ },
+ },
+ {
+ typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "C array",
+ value: (function() {
+ let buf = new (ctypes.ArrayType(ctypes.uint8_t, 15))();
+ for (let i = 0; i < 15; ++i) {
+ buf[i] = i % 256;
+ }
+ return buf;
+ })(),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_array(candidate, prefix) {
+ let cast = ctypes.cast(candidate, ctypes.uint8_t.ptr);
+ for (let i = 0; i < 15; ++i) {
+ is(
+ cast.contents,
+ i % 256,
+ prefix +
+ "Checking that the contents of the C array were preserved, index " +
+ i
+ );
+ cast = cast.increment();
+ }
+ },
+ },
+ {
+ typename: "OS.File.Error",
+ valuedescr: "OS Error",
+ type: OS.File.Error,
+ value: new OS.File.Error("foo", 1),
+ check: function check_error(candidate, prefix) {
+ ok(
+ candidate instanceof OS.File.Error,
+ prefix + "Error is an OS.File.Error"
+ );
+ ok(
+ candidate.unixErrno == 1 || candidate.winLastError == 1,
+ prefix + "Error code is correct"
+ );
+ try {
+ let string = candidate.toString();
+ info(prefix + ".toString() works " + string);
+ } catch (x) {
+ ok(false, prefix + ".toString() fails " + x);
+ }
+ },
+ },
+ ];
+ samples.forEach(function test(sample) {
+ let type = sample.type;
+ let value = sample.value;
+ let check = sample.check;
+ info(
+ "Testing handling of type " +
+ sample.typename +
+ " communicating " +
+ sample.valuedescr
+ );
+
+ // 1. Test serialization
+ let serialized;
+ let exn;
+ try {
+ serialized = type.toMsg(value);
+ } catch (ex) {
+ exn = ex;
+ }
+ is(
+ exn,
+ null,
+ "Can I serialize the following value? " +
+ value +
+ " aka " +
+ JSON.stringify(value)
+ );
+ if (exn) {
+ return;
+ }
+
+ if ("data" in serialized) {
+ // Unwrap from `Meta`
+ serialized = serialized.data;
+ }
+
+ // 2. Test deserialization
+ let deserialized;
+ try {
+ deserialized = type.fromMsg(serialized);
+ } catch (ex) {
+ exn = ex;
+ }
+ is(
+ exn,
+ null,
+ "Can I deserialize the following message? " +
+ serialized +
+ " aka " +
+ JSON.stringify(serialized)
+ );
+ if (exn) {
+ return;
+ }
+
+ // 3. Local test deserialized value
+ info(
+ "Running test on deserialized value " +
+ deserialized +
+ " aka " +
+ JSON.stringify(deserialized)
+ );
+ check(deserialized, "Local test: ");
+
+ // 4. Test sending serialized
+ info("Attempting to send message");
+ try {
+ self.postMessage({
+ kind: "value",
+ typename: sample.typename,
+ value: serialized,
+ check: check.toSource(),
+ });
+ } catch (ex) {
+ exn = ex;
+ }
+ is(
+ exn,
+ null,
+ "Can I send the following message? " +
+ serialized +
+ " aka " +
+ JSON.stringify(serialized)
+ );
+ });
+
+ finish();
+};
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js
new file mode 100644
index 0000000000..4f7e590102
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js
@@ -0,0 +1,696 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/chrome-worker, node */
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+/* import-globals-from /toolkit/components/workerloader/require.js */
+importScripts("resource://gre/modules/workers/require.js");
+
+var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+SharedAll.Config.DEBUG = true;
+
+function should_throw(f) {
+ try {
+ f();
+ } catch (x) {
+ return x;
+ }
+ return null;
+}
+
+self.onmessage = function onmessage_start(msg) {
+ self.onmessage = function onmessage_ignored(msg) {
+ log("ignored message " + JSON.stringify(msg.data));
+ };
+ try {
+ test_init();
+ test_open_existing_file();
+ test_open_non_existing_file();
+ test_flush_open_file();
+ test_copy_existing_file();
+ test_position();
+ test_move_file();
+ test_iter_dir();
+ test_info();
+ test_path();
+ test_exists_file();
+ test_remove_file();
+ } catch (x) {
+ log("Catching error: " + x);
+ log("Stack: " + x.stack);
+ log("Source: " + x.toSource());
+ ok(false, x.toString() + "\n" + x.stack);
+ }
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ /* import-globals-from /toolkit/components/osfile/osfile.jsm */
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+/**
+ * Test that we can open an existing file.
+ */
+function test_open_existing_file() {
+ info("Starting test_open_existing");
+ let file = OS.File.open(
+ "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js"
+ );
+ file.close();
+}
+
+/**
+ * Test that opening a file that does not exist fails with the right error.
+ */
+function test_open_non_existing_file() {
+ info("Starting test_open_non_existing");
+ let exn;
+ try {
+ OS.File.open("/I do not exist");
+ } catch (x) {
+ exn = x;
+ info("test_open_non_existing_file: Exception detail " + exn);
+ }
+ ok(!!exn, "test_open_non_existing_file: Exception was raised ");
+ ok(
+ exn instanceof OS.File.Error,
+ "test_open_non_existing_file: Exception was a OS.File.Error"
+ );
+ ok(
+ exn.becauseNoSuchFile,
+ "test_open_non_existing_file: Exception confirms that the file does not exist"
+ );
+}
+
+/**
+ * Test that to ensure that |foo.flush()| does not
+ * cause an error, where |foo| is an open file.
+ */
+function test_flush_open_file() {
+ info("Starting test_flush_open_file");
+ let tmp = "test_flush.tmp";
+ let file = OS.File.open(tmp, { create: true, write: true });
+ file.flush();
+ file.close();
+ OS.File.remove(tmp);
+}
+
+/**
+ * Utility function for comparing two files (or a prefix of two files).
+ *
+ * This function returns nothing but fails of both files (or prefixes)
+ * are not identical.
+ *
+ * @param {string} test The name of the test (used for logging).
+ * @param {string} sourcePath The name of the first file.
+ * @param {string} destPath The name of the second file.
+ * @param {number=} prefix If specified, only compare the |prefix|
+ * first bytes of |sourcePath| and |destPath|.
+ */
+function compare_files(test, sourcePath, destPath, prefix) {
+ info(test + ": Comparing " + sourcePath + " and " + destPath);
+ let source = OS.File.open(sourcePath);
+ let dest = OS.File.open(destPath);
+ info("Files are open");
+ let sourceResult, destResult;
+ try {
+ if (prefix != undefined) {
+ sourceResult = source.read(prefix);
+ destResult = dest.read(prefix);
+ } else {
+ sourceResult = source.read();
+ destResult = dest.read();
+ }
+ is(
+ sourceResult.length,
+ destResult.length,
+ test + ": Both files have the same size"
+ );
+ for (let i = 0; i < sourceResult.length; ++i) {
+ if (sourceResult[i] != destResult[i]) {
+ is(sourceResult[i] != destResult[i], test + ": Comparing char " + i);
+ break;
+ }
+ }
+ } finally {
+ source.close();
+ dest.close();
+ }
+ info(test + ": Comparison complete");
+}
+
+/**
+ * Test that copying a file using |copy| works.
+ */
+function test_copy_existing_file() {
+ let src_file_name = OS.Path.join(
+ "chrome",
+ "toolkit",
+ "components",
+ "osfile",
+ "tests",
+ "mochi",
+ "worker_test_osfile_front.js"
+ );
+ let tmp_file_name = "test_osfile_front.tmp";
+ info("Starting test_copy_existing");
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ info("test_copy_existing: Copy complete");
+ compare_files("test_copy_existing", src_file_name, tmp_file_name);
+
+ // Create a bogus file with arbitrary content, then attempt to overwrite
+ // it with |copy|.
+ let dest = OS.File.open(tmp_file_name, { trunc: true });
+ let buf = new Uint8Array(50);
+ dest.write(buf);
+ dest.close();
+
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ compare_files("test_copy_existing 2", src_file_name, tmp_file_name);
+
+ // Attempt to overwrite with noOverwrite
+ let exn;
+ try {
+ OS.File.copy(src_file_name, tmp_file_name, { noOverwrite: true });
+ } catch (x) {
+ exn = x;
+ }
+ ok(
+ !!exn,
+ "test_copy_existing: noOverwrite prevents overwriting existing files"
+ );
+
+ info("test_copy_existing: Cleaning up");
+ OS.File.remove(tmp_file_name);
+}
+
+/**
+ * Test that moving a file works.
+ */
+function test_move_file() {
+ info("test_move_file: Starting");
+ // 1. Copy file into a temporary file
+ let src_file_name = OS.Path.join(
+ "chrome",
+ "toolkit",
+ "components",
+ "osfile",
+ "tests",
+ "mochi",
+ "worker_test_osfile_front.js"
+ );
+ let tmp_file_name = "test_osfile_front.tmp";
+ let tmp2_file_name = "test_osfile_front.tmp2";
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ info("test_move_file: Copy complete");
+
+ // 2. Move
+ OS.File.move(tmp_file_name, tmp2_file_name);
+
+ info("test_move_file: Move complete");
+
+ // 3. Check that destination exists
+ compare_files("test_move_file", src_file_name, tmp2_file_name);
+
+ // 4. Check that original file does not exist anymore
+ let exn;
+ try {
+ OS.File.open(tmp_file_name);
+ } catch (x) {
+ exn = x;
+ }
+ ok(!!exn, "test_move_file: Original file has been removed");
+
+ info("test_move_file: Cleaning up");
+ OS.File.remove(tmp2_file_name);
+}
+
+function test_iter_dir() {
+ info("test_iter_dir: Starting");
+
+ // Create a file, to be sure that it exists
+ let tmp_file_name = "test_osfile_front.tmp";
+ let tmp_file = OS.File.open(tmp_file_name, { write: true, trunc: true });
+ tmp_file.close();
+
+ let parent = OS.File.getCurrentDirectory();
+ info("test_iter_dir: directory " + parent);
+ let iterator = new OS.File.DirectoryIterator(parent);
+ info("test_iter_dir: iterator created");
+ let encountered_tmp_file = false;
+ for (let entry of iterator) {
+ // Checking that |name| can be decoded properly
+ info("test_iter_dir: encountering entry " + entry.name);
+
+ if (entry.name == tmp_file_name) {
+ encountered_tmp_file = true;
+ isnot(
+ entry.isDir,
+ "test_iter_dir: The temporary file is not a directory"
+ );
+ isnot(entry.isSymLink, "test_iter_dir: The temporary file is not a link");
+ }
+
+ let file;
+ let success = true;
+ try {
+ file = OS.File.open(entry.path);
+ } catch (x) {
+ if (x.becauseNoSuchFile) {
+ success = false;
+ }
+ }
+ if (file) {
+ file.close();
+ }
+ ok(success, "test_iter_dir: Entry " + entry.path + " exists");
+
+ if (OS.Win) {
+ // We assume that the files are at least as recent as 2009.
+ // Since this test was written in 2011 and some of our packaging
+ // sets dates arbitrarily to 2010, this should be safe.
+ let year = new Date().getFullYear();
+
+ let lastWrite = entry.winLastWriteDate;
+ ok(
+ lastWrite,
+ "test_iter_dir: Windows lastWrite date exists: " + lastWrite
+ );
+ ok(
+ lastWrite.getFullYear() >= 2009 && lastWrite.getFullYear() <= year,
+ "test_iter_dir: consistent lastWrite date"
+ );
+
+ let lastAccess = entry.winLastAccessDate;
+ ok(
+ lastAccess,
+ "test_iter_dir: Windows lastAccess date exists: " + lastAccess
+ );
+ ok(
+ lastAccess.getFullYear() >= 2009 && lastAccess.getFullYear() <= year,
+ "test_iter_dir: consistent lastAccess date"
+ );
+ }
+ }
+ ok(encountered_tmp_file, "test_iter_dir: We have found the temporary file");
+
+ info("test_iter_dir: Cleaning up");
+ iterator.close();
+
+ // Testing nextBatch()
+ iterator = new OS.File.DirectoryIterator(parent);
+ let allentries = [];
+ for (let x of iterator) {
+ allentries.push(x);
+ }
+ iterator.close();
+
+ ok(
+ allentries.length >= 14,
+ "test_iter_dir: Meta-check: the test directory should contain at least 14 items"
+ );
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ let firstten = iterator.nextBatch(10);
+ is(firstten.length, 10, "test_iter_dir: nextBatch(10) returns 10 items");
+ for (let i = 0; i < firstten.length; ++i) {
+ is(
+ allentries[i].path,
+ firstten[i].path,
+ "test_iter_dir: Checking that batch returns the correct entries"
+ );
+ }
+ let nextthree = iterator.nextBatch(3);
+ is(nextthree.length, 3, "test_iter_dir: nextBatch(3) returns 3 items");
+ for (let i = 0; i < nextthree.length; ++i) {
+ is(
+ allentries[i + firstten.length].path,
+ nextthree[i].path,
+ "test_iter_dir: Checking that batch 2 returns the correct entries"
+ );
+ }
+ let everythingelse = iterator.nextBatch();
+ ok(
+ everythingelse.length >= 1,
+ "test_iter_dir: nextBatch() returns at least one item"
+ );
+ for (let i = 0; i < everythingelse.length; ++i) {
+ is(
+ allentries[i + firstten.length + nextthree.length].path,
+ everythingelse[i].path,
+ "test_iter_dir: Checking that batch 3 returns the correct entries"
+ );
+ }
+ is(
+ iterator.nextBatch().length,
+ 0,
+ "test_iter_dir: Once there is nothing left, nextBatch returns an empty array"
+ );
+ iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ iterator.close();
+ is(
+ iterator.nextBatch().length,
+ 0,
+ "test_iter_dir: nextBatch on closed iterator returns an empty array"
+ );
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ let allentries2 = iterator.nextBatch();
+ is(
+ allentries.length,
+ allentries2.length,
+ "test_iter_dir: Checking that getBatch(null) returns the right number of entries"
+ );
+ for (let i = 0; i < allentries.length; ++i) {
+ is(
+ allentries[i].path,
+ allentries2[i].path,
+ "test_iter_dir: Checking that getBatch(null) returns everything in the right order"
+ );
+ }
+ iterator.close();
+
+ // Test forEach
+ iterator = new OS.File.DirectoryIterator(parent);
+ let index = 0;
+ iterator.forEach(function cb(entry, aIndex, aIterator) {
+ is(index, aIndex, "test_iter_dir: Checking that forEach index is correct");
+ ok(
+ iterator == aIterator,
+ "test_iter_dir: Checking that right iterator is passed"
+ );
+ if (index < 10) {
+ is(
+ allentries[index].path,
+ entry.path,
+ "test_iter_dir: Checking that forEach entry is correct"
+ );
+ } else if (index == 10) {
+ iterator.close();
+ } else {
+ ok(false, "test_iter_dir: Checking that forEach can be stopped early");
+ }
+ ++index;
+ });
+ iterator.close();
+
+ // test for prototype |OS.File.DirectoryIterator.unixAsFile|
+ if ("unixAsFile" in OS.File.DirectoryIterator.prototype) {
+ info("testing property unixAsFile");
+ let path = OS.Path.join(
+ "chrome",
+ "toolkit",
+ "components",
+ "osfile",
+ "tests",
+ "mochi"
+ );
+ iterator = new OS.File.DirectoryIterator(path);
+
+ let dir_file = iterator.unixAsFile(); // return |File|
+ let stat0 = dir_file.stat();
+ let stat1 = OS.File.stat(path);
+
+ let unix_info_to_string = function unix_info_to_string(info) {
+ return (
+ "| " +
+ info.unixMode +
+ " | " +
+ info.unixOwner +
+ " | " +
+ info.unixGroup +
+ " | " +
+ info.lastModificationDate +
+ " | " +
+ info.lastAccessDate +
+ " | " +
+ info.size +
+ " |"
+ );
+ };
+
+ let s0_string = unix_info_to_string(stat0);
+ let s1_string = unix_info_to_string(stat1);
+
+ ok(stat0.isDir, "unixAsFile returned a directory");
+ is(s0_string, s1_string, "unixAsFile returned the correct file");
+ dir_file.close();
+ iterator.close();
+ }
+ info("test_iter_dir: Complete");
+}
+
+function test_position() {
+ info("test_position: Starting");
+
+ ok("POS_START" in OS.File, "test_position: POS_START exists");
+ ok("POS_CURRENT" in OS.File, "test_position: POS_CURRENT exists");
+ ok("POS_END" in OS.File, "test_position: POS_END exists");
+
+ let ARBITRARY_POSITION = 321;
+ let src_file_name = OS.Path.join(
+ "chrome",
+ "toolkit",
+ "components",
+ "osfile",
+ "tests",
+ "mochi",
+ "worker_test_osfile_front.js"
+ );
+
+ let file = OS.File.open(src_file_name);
+ is(file.getPosition(), 0, "test_position: Initial position is 0");
+
+ let size = 0 + file.stat().size; // Hack: We can remove this 0 + once 776259 has landed
+
+ file.setPosition(ARBITRARY_POSITION, OS.File.POS_START);
+ is(
+ file.getPosition(),
+ ARBITRARY_POSITION,
+ "test_position: Setting position from start"
+ );
+
+ file.setPosition(0, OS.File.POS_START);
+ is(
+ file.getPosition(),
+ 0,
+ "test_position: Setting position from start back to 0"
+ );
+
+ file.setPosition(ARBITRARY_POSITION);
+ is(
+ file.getPosition(),
+ ARBITRARY_POSITION,
+ "test_position: Setting position without argument"
+ );
+
+ file.setPosition(-ARBITRARY_POSITION, OS.File.POS_END);
+ is(
+ file.getPosition(),
+ size - ARBITRARY_POSITION,
+ "test_position: Setting position from end"
+ );
+
+ file.setPosition(ARBITRARY_POSITION, OS.File.POS_CURRENT);
+ is(file.getPosition(), size, "test_position: Setting position from current");
+
+ file.close();
+ info("test_position: Complete");
+}
+
+function test_info() {
+ info("test_info: Starting");
+
+ let filename = "test_info.tmp";
+ let size = 261; // An arbitrary file length
+ let start = new Date();
+
+ // Cleanup any leftover from previous tests
+ try {
+ OS.File.remove(filename);
+ info("test_info: Cleaned up previous garbage");
+ } catch (x) {
+ if (!x.becauseNoSuchFile) {
+ throw x;
+ }
+ info("test_info: No previous garbage");
+ }
+
+ let file = OS.File.open(filename, { trunc: true });
+ let buf = new ArrayBuffer(size);
+ file._write(buf, size);
+ file.close();
+
+ // Test OS.File.stat on new file
+ let stat = OS.File.stat(filename);
+ ok(!!stat, "test_info: info acquired");
+ ok(!stat.isDir, "test_info: file is not a directory");
+ is(stat.isSymLink, false, "test_info: file is not a link");
+ is(stat.size.toString(), size, "test_info: correct size");
+
+ let stop = new Date();
+
+ // We round down/up by 1s as file system precision is lower than
+ // Date precision (no clear specifications about that, but it seems
+ // that this can be a little over 1 second under ext3 and 2 seconds
+ // under FAT).
+ let SLOPPY_FILE_SYSTEM_ADJUSTMENT = 3000;
+ let startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ let stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ info("Testing stat with bounds [ " + startMs + ", " + stopMs + " ]");
+
+ let change = stat.lastModificationDate;
+ info("Testing lastModificationDate: " + change);
+ ok(
+ change.getTime() >= startMs && change.getTime() <= stopMs,
+ "test_info: lastModificationDate is consistent"
+ );
+
+ // Test OS.File.prototype.stat on new file
+ file = OS.File.open(filename);
+ try {
+ stat = file.stat();
+ } finally {
+ file.close();
+ }
+
+ ok(!!stat, "test_info: info acquired 2");
+ ok(!stat.isDir, "test_info: file is not a directory 2");
+ ok(!stat.isSymLink, "test_info: file is not a link 2");
+ is(stat.size.toString(), size, "test_info: correct size 2");
+
+ stop = new Date();
+
+ // Round up/down as above
+ startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ info("Testing stat 2 with bounds [ " + startMs + ", " + stopMs + " ]");
+
+ let access = stat.lastAccessDate;
+ info("Testing lastAccessDate: " + access);
+ ok(
+ access.getTime() >= startMs && access.getTime() <= stopMs,
+ "test_info: lastAccessDate is consistent"
+ );
+
+ change = stat.lastModificationDate;
+ info("Testing lastModificationDate 2: " + change);
+ ok(
+ change.getTime() >= startMs && change.getTime() <= stopMs,
+ "test_info: lastModificationDate 2 is consistent"
+ );
+
+ // Test OS.File.stat on directory
+ stat = OS.File.stat(OS.File.getCurrentDirectory());
+ ok(!!stat, "test_info: info on directory acquired");
+ ok(stat.isDir, "test_info: directory is a directory");
+
+ info("test_info: Complete");
+}
+
+// Note that most of the features of path are tested in
+// worker_test_osfile_{unix, win}.js
+function test_path() {
+ info("test_path: starting");
+ let abcd = OS.Path.join("a", "b", "c", "d");
+ is(OS.Path.basename(abcd), "d", "basename of a/b/c/d");
+
+ let abc = OS.Path.join("a", "b", "c");
+ is(OS.Path.dirname(abcd), abc, "dirname of a/b/c/d");
+
+ let abdotsc = OS.Path.join("a", "b", "..", "c");
+ is(OS.Path.normalize(abdotsc), OS.Path.join("a", "c"), "normalize a/b/../c");
+
+ let adotsdotsdots = OS.Path.join("a", "..", "..", "..");
+ is(
+ OS.Path.normalize(adotsdotsdots),
+ OS.Path.join("..", ".."),
+ "normalize a/../../.."
+ );
+
+ info("test_path: Complete");
+}
+
+/**
+ * Test the file |exists| method.
+ */
+function test_exists_file() {
+ let file_name = OS.Path.join(
+ "chrome",
+ "toolkit",
+ "components",
+ "osfile",
+ "tests",
+ "mochi",
+ "test_osfile_front.xhtml"
+ );
+ info("test_exists_file: starting");
+ ok(
+ OS.File.exists(file_name),
+ "test_exists_file: file exists (OS.File.exists)"
+ );
+ ok(
+ !OS.File.exists(file_name + ".tmp"),
+ "test_exists_file: file does not exists (OS.File.exists)"
+ );
+
+ let dir_name = OS.Path.join(
+ "chrome",
+ "toolkit",
+ "components",
+ "osfile",
+ "tests",
+ "mochi"
+ );
+ ok(OS.File.exists(dir_name), "test_exists_file: directory exists");
+ ok(
+ !OS.File.exists(dir_name) + ".tmp",
+ "test_exists_file: directory does not exist"
+ );
+
+ info("test_exists_file: complete");
+}
+
+/**
+ * Test the file |remove| method.
+ */
+function test_remove_file() {
+ let absent_file_name = "test_osfile_front_absent.tmp";
+
+ // Check that removing absent files is handled correctly
+ let exn = should_throw(function() {
+ OS.File.remove(absent_file_name, { ignoreAbsent: false });
+ });
+ ok(!!exn, "test_remove_file: throws if there is no such file");
+
+ exn = should_throw(function() {
+ OS.File.remove(absent_file_name, { ignoreAbsent: true });
+ OS.File.remove(absent_file_name);
+ });
+ ok(!exn, "test_remove_file: ignoreAbsent works");
+
+ if (OS.Win) {
+ let file_name = "test_osfile_front_file_to_remove.tmp";
+ let file = OS.File.open(file_name, { write: true });
+ file.close();
+ ok(OS.File.exists(file_name), "test_remove_file: test file exists");
+ OS.Win.File.SetFileAttributes(
+ file_name,
+ OS.Constants.Win.FILE_ATTRIBUTE_READONLY
+ );
+ OS.File.remove(file_name);
+ ok(
+ !OS.File.exists(file_name),
+ "test_remove_file: test file has been removed"
+ );
+ }
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js
new file mode 100644
index 0000000000..aa456da030
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/chrome-worker, node */
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+
+self.onmessage = function(msg) {
+ log("received message " + JSON.stringify(msg.data));
+ self.onmessage = function(msg) {
+ log("ignored message " + JSON.stringify(msg.data));
+ };
+ test_init();
+ test_getcwd();
+ test_open_close();
+ test_create_file();
+ test_access();
+ test_read_write();
+ test_passing_undefined();
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ /* import-globals-from /toolkit/components/osfile/osfile.jsm */
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+function test_open_close() {
+ info("Starting test_open_close");
+ is(typeof OS.Unix.File.open, "function", "OS.Unix.File.open is a function");
+ let file = OS.Unix.File.open(
+ "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js",
+ OS.Constants.libc.O_RDONLY
+ );
+ isnot(file, -1, "test_open_close: opening succeeded");
+ info("Close: " + OS.Unix.File.close.toSource());
+ let result = OS.Unix.File.close(file);
+ is(result, 0, "test_open_close: close succeeded");
+
+ file = OS.Unix.File.open("/i do not exist", OS.Constants.libc.O_RDONLY);
+ is(file, -1, "test_open_close: opening of non-existing file failed");
+ is(
+ ctypes.errno,
+ OS.Constants.libc.ENOENT,
+ "test_open_close: error is ENOENT"
+ );
+}
+
+function test_create_file() {
+ info("Starting test_create_file");
+ let file = OS.Unix.File.open(
+ "test.tmp",
+ OS.Constants.libc.O_RDWR |
+ OS.Constants.libc.O_CREAT |
+ OS.Constants.libc.O_TRUNC,
+ ctypes.int(OS.Constants.libc.S_IRWXU)
+ );
+ isnot(file, -1, "test_create_file: file created");
+ OS.Unix.File.close(file);
+}
+
+function test_access() {
+ info("Starting test_access");
+ let file = OS.Unix.File.open(
+ "test1.tmp",
+ OS.Constants.libc.O_RDWR |
+ OS.Constants.libc.O_CREAT |
+ OS.Constants.libc.O_TRUNC,
+ ctypes.int(OS.Constants.libc.S_IRWXU)
+ );
+ let result = OS.Unix.File.access(
+ "test1.tmp",
+ OS.Constants.libc.R_OK |
+ OS.Constants.libc.W_OK |
+ OS.Constants.libc.X_OK |
+ OS.Constants.libc.F_OK
+ );
+ is(result, 0, "first call to access() succeeded");
+ OS.Unix.File.close(file);
+
+ file = OS.Unix.File.open(
+ "test1.tmp",
+ OS.Constants.libc.O_WRONLY |
+ OS.Constants.libc.O_CREAT |
+ OS.Constants.libc.O_TRUNC,
+ ctypes.int(OS.Constants.libc.S_IWUSR)
+ );
+
+ info("test_access: preparing second call to access()");
+ result = OS.Unix.File.access(
+ "test2.tmp",
+ OS.Constants.libc.R_OK |
+ OS.Constants.libc.W_OK |
+ OS.Constants.libc.X_OK |
+ OS.Constants.libc.F_OK
+ );
+ is(result, -1, "test_access: second call to access() failed as expected");
+ is(ctypes.errno, OS.Constants.libc.ENOENT, "This is the correct error");
+ OS.Unix.File.close(file);
+}
+
+function test_getcwd() {
+ let array = new (ctypes.ArrayType(ctypes.char, 32768))();
+ let path = OS.Unix.File.getcwd(array, array.length);
+ if (ctypes.char.ptr(path).isNull()) {
+ ok(false, "test_get_cwd: getcwd returned null, errno: " + ctypes.errno);
+ }
+ let path2;
+ if (OS.Unix.File.get_current_dir_name) {
+ path2 = OS.Unix.File.get_current_dir_name();
+ } else {
+ path2 = OS.Unix.File.getwd_auto(null);
+ }
+ if (ctypes.char.ptr(path2).isNull()) {
+ ok(
+ false,
+ "test_get_cwd: getwd_auto/get_current_dir_name returned null, errno: " +
+ ctypes.errno
+ );
+ }
+ is(
+ path.readString(),
+ path2.readString(),
+ "test_get_cwd: getcwd and getwd return the same path"
+ );
+}
+
+function test_read_write() {
+ let output_name = "osfile_copy.tmp";
+ // Copy file
+ let input = OS.Unix.File.open(
+ "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js",
+ OS.Constants.libc.O_RDONLY
+ );
+ isnot(input, -1, "test_read_write: input file opened");
+ let output = OS.Unix.File.open(
+ "osfile_copy.tmp",
+ OS.Constants.libc.O_RDWR |
+ OS.Constants.libc.O_CREAT |
+ OS.Constants.libc.O_TRUNC,
+ ctypes.int(OS.Constants.libc.S_IRWXU)
+ );
+ isnot(output, -1, "test_read_write: output file opened");
+
+ let array = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes = -1;
+ let total = 0;
+ while (true) {
+ bytes = OS.Unix.File.read(input, array, 4096);
+ ok(bytes != undefined, "test_read_write: bytes is defined");
+ isnot(bytes, -1, "test_read_write: no read error");
+ let write_from = 0;
+ if (bytes == 0) {
+ break;
+ }
+ while (bytes > 0) {
+ array.addressOfElement(write_from);
+ // Note: |write| launches an exception in case of error
+ let written = OS.Unix.File.write(output, array, bytes);
+ isnot(written, -1, "test_read_write: no write error");
+ write_from += written;
+ bytes -= written;
+ }
+ total += write_from;
+ }
+ info("test_read_write: copy complete " + total);
+
+ // Compare files
+ let result;
+ info("SEEK_SET: " + OS.Constants.libc.SEEK_SET);
+ info("Input: " + input + "(" + input.toSource() + ")");
+ info("Output: " + output + "(" + output.toSource() + ")");
+ result = OS.Unix.File.lseek(input, 0, OS.Constants.libc.SEEK_SET);
+ info("Result of lseek: " + result);
+ isnot(result, -1, "test_read_write: input seek succeeded " + ctypes.errno);
+ result = OS.Unix.File.lseek(output, 0, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: output seek succeeded " + ctypes.errno);
+
+ let array2 = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes2 = -1;
+ let pos = 0;
+ while (true) {
+ bytes = OS.Unix.File.read(input, array, 4096);
+ isnot(bytes, -1, "test_read_write: input read succeeded");
+ bytes2 = OS.Unix.File.read(output, array2, 4096);
+ isnot(bytes, -1, "test_read_write: output read succeeded");
+ is(
+ bytes > 0,
+ bytes2 > 0,
+ "Both files contain data or neither does " + bytes + ", " + bytes2
+ );
+ if (bytes == 0) {
+ break;
+ }
+ if (bytes != bytes2) {
+ // This would be surprising, but theoretically possible with a
+ // remote file system, I believe.
+ bytes = Math.min(bytes, bytes2);
+ pos += bytes;
+ result = OS.Unix.File.lseek(input, pos, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: input seek succeeded");
+ result = OS.Unix.File.lseek(output, pos, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: output seek succeeded");
+ } else {
+ pos += bytes;
+ }
+ for (let i = 0; i < bytes; ++i) {
+ if (array[i] != array2[i]) {
+ ok(
+ false,
+ "Files do not match at position " +
+ i +
+ " (" +
+ array[i] +
+ "/" +
+ array2[i] +
+ ")"
+ );
+ }
+ }
+ }
+ info("test_read_write test complete");
+ result = OS.Unix.File.close(input);
+ isnot(result, -1, "test_read_write: input close succeeded");
+ result = OS.Unix.File.close(output);
+ isnot(result, -1, "test_read_write: output close succeeded");
+ result = OS.Unix.File.unlink(output_name);
+ isnot(result, -1, "test_read_write: input remove succeeded");
+ info("test_read_write cleanup complete");
+}
+
+function test_passing_undefined() {
+ info(
+ "Testing that an exception gets thrown when an FFI function is passed undefined"
+ );
+ let exceptionRaised = false;
+
+ try {
+ OS.Unix.File.open(
+ undefined,
+ OS.Constants.libc.O_RDWR |
+ OS.Constants.libc.O_CREAT |
+ OS.Constants.libc.O_TRUNC,
+ ctypes.int(OS.Constants.libc.S_IRWXU)
+ );
+ } catch (e) {
+ if (e instanceof TypeError && e.message.indexOf("open") > -1) {
+ exceptionRaised = true;
+ } else {
+ throw e;
+ }
+ }
+
+ ok(exceptionRaised, "test_passing_undefined: exception gets thrown");
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js
new file mode 100644
index 0000000000..5e02c03998
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js
@@ -0,0 +1,310 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/chrome-worker, node */
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+
+self.onmessage = function(msg) {
+ self.onmessage = function(msg) {
+ log("ignored message " + JSON.stringify(msg.data));
+ };
+
+ test_init();
+ test_GetCurrentDirectory();
+ test_OpenClose();
+ test_CreateFile();
+ test_ReadWrite();
+ test_passing_undefined();
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ /* import-globals-from /toolkit/components/osfile/osfile.jsm */
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+function test_OpenClose() {
+ info("Starting test_OpenClose");
+ is(
+ typeof OS.Win.File.CreateFile,
+ "function",
+ "OS.Win.File.CreateFile is a function"
+ );
+ is(
+ OS.Win.File.CloseHandle(OS.Constants.Win.INVALID_HANDLE_VALUE),
+ true,
+ "CloseHandle returns true given the invalid handle"
+ );
+ is(
+ OS.Win.File.FindClose(OS.Constants.Win.INVALID_HANDLE_VALUE),
+ true,
+ "FindClose returns true given the invalid handle"
+ );
+ isnot(OS.Constants.Win.GENERIC_READ, undefined, "GENERIC_READ exists");
+ isnot(OS.Constants.Win.FILE_SHARE_READ, undefined, "FILE_SHARE_READ exists");
+ isnot(
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ undefined,
+ "FILE_ATTRIBUTE_NORMAL exists"
+ );
+ let file = OS.Win.File.CreateFile(
+ "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js",
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null
+ );
+ info("test_OpenClose: Passed open");
+ isnot(
+ file,
+ OS.Constants.Win.INVALID_HANDLE_VALUE,
+ "test_OpenClose: file opened"
+ );
+ let result = OS.Win.File.CloseHandle(file);
+ isnot(result, 0, "test_OpenClose: close succeeded");
+
+ file = OS.Win.File.CreateFile(
+ "\\I do not exist",
+ OS.Constants.Win.GENERIC_READ,
+ OS.Constants.Win.FILE_SHARE_READ,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null
+ );
+ is(
+ file,
+ OS.Constants.Win.INVALID_HANDLE_VALUE,
+ "test_OpenClose: cannot open non-existing file"
+ );
+ is(
+ ctypes.winLastError,
+ OS.Constants.Win.ERROR_FILE_NOT_FOUND,
+ "test_OpenClose: error is ERROR_FILE_NOT_FOUND"
+ );
+}
+
+function test_CreateFile() {
+ info("Starting test_CreateFile");
+ let file = OS.Win.File.CreateFile(
+ "test.tmp",
+ OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE,
+ OS.Constants.Win.FILE_SHARE_READ | OS.Constants.FILE_SHARE_WRITE,
+ null,
+ OS.Constants.Win.CREATE_ALWAYS,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null
+ );
+ isnot(
+ file,
+ OS.Constants.Win.INVALID_HANDLE_VALUE,
+ "test_CreateFile: opening succeeded"
+ );
+ let result = OS.Win.File.CloseHandle(file);
+ isnot(result, 0, "test_CreateFile: close succeeded");
+}
+
+function test_GetCurrentDirectory() {
+ let array = new (ctypes.ArrayType(ctypes.char16_t, 4096))();
+ let result = OS.Win.File.GetCurrentDirectory(4096, array);
+ ok(result < array.length, "test_GetCurrentDirectory: length sufficient");
+ ok(result > 0, "test_GetCurrentDirectory: length != 0");
+}
+
+function test_ReadWrite() {
+ info("Starting test_ReadWrite");
+ let output_name = "osfile_copy.tmp";
+ // Copy file
+ let input = OS.Win.File.CreateFile(
+ "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js",
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null
+ );
+ isnot(
+ input,
+ OS.Constants.Win.INVALID_HANDLE_VALUE,
+ "test_ReadWrite: input file opened"
+ );
+ let output = OS.Win.File.CreateFile(
+ "osfile_copy.tmp",
+ OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE,
+ 0,
+ null,
+ OS.Constants.Win.CREATE_ALWAYS,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null
+ );
+ isnot(
+ output,
+ OS.Constants.Win.INVALID_HANDLE_VALUE,
+ "test_ReadWrite: output file opened"
+ );
+ let array = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes_read = new ctypes.uint32_t(0);
+ let bytes_read_ptr = bytes_read.address();
+ log("We have a pointer for bytes read: " + bytes_read_ptr);
+ let bytes_written = new ctypes.uint32_t(0);
+ let bytes_written_ptr = bytes_written.address();
+ log("We have a pointer for bytes written: " + bytes_written_ptr);
+ log("test_ReadWrite: buffer and pointers ready");
+ let result;
+ while (true) {
+ log("test_ReadWrite: reading");
+ result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null);
+ isnot(result, 0, "test_ReadWrite: read success");
+ let write_from = 0;
+ let bytes_left = bytes_read;
+ log("test_ReadWrite: read chunk complete " + bytes_left.value);
+ if (bytes_left.value == 0) {
+ break;
+ }
+ while (bytes_left.value > 0) {
+ log("test_ReadWrite: writing " + bytes_left.value);
+ array.addressOfElement(write_from);
+ // Note: |WriteFile| launches an exception in case of error
+ result = OS.Win.File.WriteFile(
+ output,
+ array,
+ bytes_left,
+ bytes_written_ptr,
+ null
+ );
+ isnot(result, 0, "test_ReadWrite: write success");
+ write_from += bytes_written;
+ bytes_left -= bytes_written;
+ }
+ }
+ info("test_ReadWrite: copy complete");
+
+ // Compare files
+ result = OS.Win.File.SetFilePointer(
+ input,
+ 0,
+ null,
+ OS.Constants.Win.FILE_BEGIN
+ );
+ isnot(
+ result,
+ OS.Constants.Win.INVALID_SET_FILE_POINTER,
+ "test_ReadWrite: input reset"
+ );
+
+ result = OS.Win.File.SetFilePointer(
+ output,
+ 0,
+ null,
+ OS.Constants.Win.FILE_BEGIN
+ );
+ isnot(
+ result,
+ OS.Constants.Win.INVALID_SET_FILE_POINTER,
+ "test_ReadWrite: output reset"
+ );
+
+ let array2 = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes_read2 = new ctypes.uint32_t(0);
+ let bytes_read2_ptr = bytes_read2.address();
+ let pos = 0;
+ while (true) {
+ result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null);
+ isnot(result, 0, "test_ReadWrite: input read succeeded");
+
+ result = OS.Win.File.ReadFile(output, array2, 4096, bytes_read2_ptr, null);
+ isnot(result, 0, "test_ReadWrite: output read succeeded");
+
+ is(
+ bytes_read.value > 0,
+ bytes_read2.value > 0,
+ "Both files contain data or neither does " +
+ bytes_read.value +
+ ", " +
+ bytes_read2.value
+ );
+ if (bytes_read.value == 0) {
+ break;
+ }
+ let bytes;
+ if (bytes_read.value != bytes_read2.value) {
+ // This would be surprising, but theoretically possible with a
+ // remote file system, I believe.
+ bytes = Math.min(bytes_read.value, bytes_read2.value);
+ pos += bytes;
+ result = OS.Win.File.SetFilePointer(
+ input,
+ pos,
+ null,
+ OS.Constants.Win.FILE_BEGIN
+ );
+ isnot(result, 0, "test_ReadWrite: input seek succeeded");
+
+ result = OS.Win.File.SetFilePointer(
+ output,
+ pos,
+ null,
+ OS.Constants.Win.FILE_BEGIN
+ );
+ isnot(result, 0, "test_ReadWrite: output seek succeeded");
+ } else {
+ bytes = bytes_read.value;
+ pos += bytes;
+ }
+ for (let i = 0; i < bytes; ++i) {
+ if (array[i] != array2[i]) {
+ ok(
+ false,
+ "Files do not match at position " +
+ i +
+ " (" +
+ array[i] +
+ "/" +
+ array2[i] +
+ ")"
+ );
+ }
+ }
+ }
+ info("test_ReadWrite test complete");
+ result = OS.Win.File.CloseHandle(input);
+ isnot(result, 0, "test_ReadWrite: inpout close succeeded");
+ result = OS.Win.File.CloseHandle(output);
+ isnot(result, 0, "test_ReadWrite: outpout close succeeded");
+ result = OS.Win.File.DeleteFile(output_name);
+ isnot(result, 0, "test_ReadWrite: output remove succeeded");
+ info("test_ReadWrite cleanup complete");
+}
+
+function test_passing_undefined() {
+ info(
+ "Testing that an exception gets thrown when an FFI function is passed undefined"
+ );
+ let exceptionRaised = false;
+
+ try {
+ OS.Win.File.CreateFile(
+ undefined,
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null
+ );
+ } catch (e) {
+ if (e instanceof TypeError && e.message.indexOf("CreateFile") > -1) {
+ exceptionRaised = true;
+ } else {
+ throw e;
+ }
+ }
+
+ ok(exceptionRaised, "test_passing_undefined: exception gets thrown");
+}
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]