summaryrefslogtreecommitdiffstats
path: root/dom/system/tests/ioutils
diff options
context:
space:
mode:
Diffstat (limited to 'dom/system/tests/ioutils')
-rw-r--r--dom/system/tests/ioutils/chrome.ini16
-rw-r--r--dom/system/tests/ioutils/file_ioutils_test_fixtures.js78
-rw-r--r--dom/system/tests/ioutils/file_ioutils_worker.js102
-rw-r--r--dom/system/tests/ioutils/test_ioutils.html26
-rw-r--r--dom/system/tests/ioutils/test_ioutils_copy_move.html366
-rw-r--r--dom/system/tests/ioutils/test_ioutils_dir_iteration.html84
-rw-r--r--dom/system/tests/ioutils/test_ioutils_mkdir.html113
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write.html432
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_json.html153
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_utf8.html389
-rw-r--r--dom/system/tests/ioutils/test_ioutils_remove.html97
-rw-r--r--dom/system/tests/ioutils/test_ioutils_set_permissions.html56
-rw-r--r--dom/system/tests/ioutils/test_ioutils_stat_touch.html204
-rw-r--r--dom/system/tests/ioutils/test_ioutils_worker.xhtml40
14 files changed, 2156 insertions, 0 deletions
diff --git a/dom/system/tests/ioutils/chrome.ini b/dom/system/tests/ioutils/chrome.ini
new file mode 100644
index 0000000000..b3f109a554
--- /dev/null
+++ b/dom/system/tests/ioutils/chrome.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files =
+ file_ioutils_test_fixtures.js
+ file_ioutils_worker.js
+
+[test_ioutils.html]
+[test_ioutils_copy_move.html]
+[test_ioutils_dir_iteration.html]
+[test_ioutils_mkdir.html]
+[test_ioutils_read_write.html]
+[test_ioutils_read_write_json.html]
+[test_ioutils_read_write_utf8.html]
+[test_ioutils_remove.html]
+[test_ioutils_stat_touch.html]
+[test_ioutils_worker.xhtml]
+[test_ioutils_set_permissions.html]
diff --git a/dom/system/tests/ioutils/file_ioutils_test_fixtures.js b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js
new file mode 100644
index 0000000000..5d2e5011c9
--- /dev/null
+++ b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js
@@ -0,0 +1,78 @@
+// Utility functions.
+
+Uint8Array.prototype.equals = function equals(other) {
+ if (this.byteLength !== other.byteLength) {
+ return false;
+ }
+ return this.every((val, i) => val === other[i]);
+};
+
+async function createFile(location, contents = "") {
+ if (typeof contents === "string") {
+ contents = new TextEncoder().encode(contents);
+ }
+ await IOUtils.write(location, contents);
+ const exists = await fileExists(location);
+ ok(exists, `Created temporary file at: ${location}`);
+}
+
+async function createDir(location) {
+ await IOUtils.makeDirectory(location, {
+ ignoreExisting: true,
+ createAncestors: true,
+ });
+ const exists = await dirExists(location);
+ ok(exists, `Created temporary directory at: ${location}`);
+}
+
+async function fileHasBinaryContents(location, expectedContents) {
+ if (!(expectedContents instanceof Uint8Array)) {
+ throw new TypeError("expectedContents must be a byte array");
+ }
+ info(`Opening ${location} for reading`);
+ const bytes = await IOUtils.read(location);
+ return bytes.equals(expectedContents);
+}
+
+async function fileHasTextContents(location, expectedContents) {
+ if (typeof expectedContents !== "string") {
+ throw new TypeError("expectedContents must be a string");
+ }
+ info(`Opening ${location} for reading`);
+ const bytes = await IOUtils.read(location);
+ const contents = new TextDecoder().decode(bytes);
+ return contents === expectedContents;
+}
+
+async function fileExists(file) {
+ try {
+ let { type } = await IOUtils.stat(file);
+ return type === "regular";
+ } catch (ex) {
+ return false;
+ }
+}
+
+async function dirExists(dir) {
+ try {
+ let { type } = await IOUtils.stat(dir);
+ return type === "directory";
+ } catch (ex) {
+ return false;
+ }
+}
+
+async function cleanup(...files) {
+ for (const file of files) {
+ await IOUtils.remove(file, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+ const exists = await IOUtils.exists(file);
+ ok(!exists, `Removed temporary file: ${file}`);
+ }
+}
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
diff --git a/dom/system/tests/ioutils/file_ioutils_worker.js b/dom/system/tests/ioutils/file_ioutils_worker.js
new file mode 100644
index 0000000000..68c0e81289
--- /dev/null
+++ b/dom/system/tests/ioutils/file_ioutils_worker.js
@@ -0,0 +1,102 @@
+// Any copyright is dedicated to the Public Domain.
+// - http://creativecommons.org/publicdomain/zero/1.0/
+
+/* eslint-env mozilla/chrome-worker, node */
+/* global finish, log */
+
+"use strict";
+
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+importScripts("resource://gre/modules/ObjectUtils.jsm");
+
+// TODO: Remove this import for OS.File. It is currently being used as a
+// stop gap for missing IOUtils functionality.
+importScripts("resource://gre/modules/osfile.jsm");
+importScripts("file_ioutils_test_fixtures.js");
+
+self.onmessage = async function(msg) {
+ const tmpDir = OS.Constants.Path.tmpDir;
+
+ // IOUtils functionality is the same when called from the main thread, or a
+ // web worker. These tests are a modified subset of the main thread tests, and
+ // serve as a confidence check that the implementation is thread-safe.
+ await test_api_is_available_on_worker();
+ await test_full_read_and_write();
+ await test_move_file();
+ await test_copy_file();
+ await test_make_directory();
+
+ finish();
+ info("test_ioutils_worker.xhtml: Test finished");
+
+ async function test_api_is_available_on_worker() {
+ ok(self.IOUtils, "IOUtils is present in web workers");
+ }
+
+ async function test_full_read_and_write() {
+ // Write a file.
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_numbers.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ const bytesWritten = await IOUtils.write(tmpFileName, bytes);
+ is(bytesWritten, 50, "IOUtils::write can write entire byte array to file");
+
+ // Read it back.
+ let fileContents = await IOUtils.read(tmpFileName);
+ ok(
+ ObjectUtils.deepEqual(bytes, fileContents) &&
+ bytes.length == fileContents.length,
+ "IOUtils::read can read back entire file"
+ );
+
+ const tooManyBytes = bytes.length + 1;
+ fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes });
+ ok(
+ ObjectUtils.deepEqual(bytes, fileContents) &&
+ fileContents.length == bytes.length,
+ "IOUtils::read can read entire file when requested maxBytes is too large"
+ );
+
+ await cleanup(tmpFileName);
+ }
+
+ async function test_move_file() {
+ const src = OS.Path.join(tmpDir, "test_move_file_src.tmp");
+ const dest = OS.Path.join(tmpDir, "test_move_file_dest.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ await IOUtils.write(src, bytes);
+
+ await IOUtils.move(src, dest);
+ ok(
+ !(await fileExists(src)) && (await fileExists(dest)),
+ "IOUtils::move can move files from a worker"
+ );
+
+ await cleanup(dest);
+ }
+
+ async function test_copy_file() {
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_orig.tmp");
+ const destFileName = OS.Path.join(tmpDir, "test_ioutils_copy.tmp");
+ await createFile(tmpFileName, "original");
+
+ await IOUtils.copy(tmpFileName, destFileName);
+ ok(
+ (await fileExists(tmpFileName)) &&
+ (await fileHasTextContents(destFileName, "original")),
+ "IOUtils::copy can copy source to dest in same directory"
+ );
+
+ await cleanup(tmpFileName, destFileName);
+ }
+
+ async function test_make_directory() {
+ const dir = OS.Path.join(tmpDir, "test_make_dir.tmp.d");
+ await IOUtils.makeDirectory(dir);
+ ok(
+ OS.File.stat(dir).isDir,
+ "IOUtils::makeDirectory can make a new directory from a worker"
+ );
+
+ await cleanup(dir);
+ }
+};
diff --git a/dom/system/tests/ioutils/test_ioutils.html b/dom/system/tests/ioutils/test_ioutils.html
new file mode 100644
index 0000000000..cf62c4c388
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script>
+ "use strict";
+
+ add_task(async function test_api_is_available_on_window() {
+ ok(window.IOUtils, "IOUtils is present on the window");
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_copy_move.html b/dom/system/tests/ioutils/test_ioutils_copy_move.html
new file mode 100644
index 0000000000..239295bf7e
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html
@@ -0,0 +1,366 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ // TODO: Remove this import for OS.File. It is currently being used as a
+ // stop gap for missing IOUtils functionality.
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+
+ const tmpDir = OS.Constants.Path.tmpDir;
+
+ add_task(async function test_move_relative_path() {
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_move_relative_path.tmp");
+ const dest = "relative_to_cwd.tmp";
+ await createFile(tmpFileName, "source");
+
+ info("Test moving a file to a relative destination");
+ await Assert.rejects(
+ IOUtils.move(tmpFileName, dest),
+ /Could not parse path/,
+ "IOUtils::move only works with absolute paths"
+ );
+ ok(
+ await fileHasTextContents(tmpFileName, "source"),
+ "IOUtils::move doesn't change source file when move fails"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_move_rename() {
+ // Set up.
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_move_src.tmp");
+ const destFileName = OS.Path.join(tmpDir, "test_ioutils_move_dest.tmp");
+ await createFile(tmpFileName, "dest");
+ // Test.
+ info("Test move to new file in same directory");
+ await IOUtils.move(tmpFileName, destFileName);
+ info(`Moved ${tmpFileName} to ${destFileName}`);
+ ok(
+ !await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "dest"),
+ "IOUtils::move can move source to dest in same directory"
+ )
+
+ // Set up.
+ info("Test move to existing file with no overwrite");
+ await createFile(tmpFileName, "source");
+ // Test.
+ await Assert.rejects(
+ IOUtils.move(tmpFileName, destFileName, { noOverwrite: true }),
+ /Could not move source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/,
+ "IOUtils::move will refuse to move a file if overwrites are disabled"
+ );
+ ok(
+ await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "dest"),
+ "Failed IOUtils::move doesn't move the source file"
+ );
+
+ // Test.
+ info("Test move to existing file with overwrite");
+ await IOUtils.move(tmpFileName, destFileName, { noOverwrite: false });
+ ok(!await fileExists(tmpFileName), "IOUtils::move moved source");
+ ok(
+ await fileHasTextContents(destFileName, "source"),
+ "IOUtils::move overwrote the destination with the source"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName, destFileName);
+ });
+
+ add_task(async function test_move_to_dir() {
+ // Set up.
+ info("Test move and rename to non-existing directory");
+ const tmpFileName = OS.Path.join(tmpDir, "test_move_to_dir.tmp");
+ const destDir = OS.Path.join(tmpDir, "test_move_to_dir.tmp.d");
+ const dest = OS.Path.join(destDir, "dest.tmp");
+ await createFile(tmpFileName);
+ // Test.
+ ok(!await IOUtils.exists(destDir), "Expected path not to exist");
+ await IOUtils.move(tmpFileName, dest);
+ ok(
+ !await fileExists(tmpFileName) && await fileExists(dest),
+ "IOUtils::move creates non-existing parents if needed"
+ );
+
+ // Set up.
+ info("Test move and rename to existing directory.")
+ await createFile(tmpFileName);
+ // Test.
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ await IOUtils.move(tmpFileName, dest);
+ ok(
+ !await fileExists(tmpFileName)
+ && await fileExists(dest),
+ "IOUtils::move can move/rename a file into an existing dir"
+ );
+
+ // Set up.
+ info("Test move to existing directory without specifying leaf name.")
+ await createFile(tmpFileName);
+ // Test.
+ await IOUtils.move(tmpFileName, destDir);
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ ok(
+ !await fileExists(tmpFileName)
+ && await fileExists(OS.Path.join(destDir, OS.Path.basename(tmpFileName))),
+ "IOUtils::move can move a file into an existing dir"
+ );
+
+ // Clean up.
+ await cleanup(destDir);
+ });
+
+ add_task(async function test_move_dir() {
+ // Set up.
+ info("Test rename an empty directory");
+ const srcDir = OS.Path.join(tmpDir, "test_move_dir.tmp.d");
+ const destDir = OS.Path.join(tmpDir, "test_move_dir_dest.tmp.d");
+ await createDir(srcDir);
+ // Test.
+ await IOUtils.move(srcDir, destDir);
+ ok(
+ !await IOUtils.exists(srcDir) && await dirExists(destDir),
+ "IOUtils::move can rename directories"
+ );
+
+ // Set up.
+ info("Test move directory and its content into another directory");
+ await createDir(srcDir);
+ await createFile(OS.Path.join(srcDir, "file.tmp"), "foo");
+ // Test.
+ await IOUtils.move(srcDir, destDir);
+ const destFile = OS.Path.join(destDir, OS.Path.basename(srcDir), "file.tmp");
+ ok(
+ !await IOUtils.exists(srcDir)
+ && await dirExists(destDir)
+ && await dirExists(OS.Path.join(destDir, OS.Path.basename(srcDir)))
+ && await fileHasTextContents(destFile, "foo"),
+ "IOUtils::move can move a directory and its contents into another one"
+ )
+
+ // Clean up.
+ await cleanup(srcDir, destDir);
+ });
+
+ add_task(async function test_move_failures() {
+ // Set up.
+ info("Test attempt to rename a non-existent source file");
+ const notExistsSrc = OS.Path.join(tmpDir, "not_exists_src.tmp");
+ const notExistsDest = OS.Path.join(tmpDir, "not_exists_dest.tmp");
+ // Test.
+ await Assert.rejects(
+ IOUtils.move(notExistsSrc, notExistsDest),
+ /Could not move source file\(.*\) because it does not exist/,
+ "IOUtils::move throws if source file does not exist"
+ );
+ ok(
+ !await fileExists(notExistsSrc) && !await fileExists(notExistsDest),
+ "IOUtils::move fails if source file does not exist"
+ );
+
+ // Set up.
+ info("Test attempt to move a directory to a file");
+ const destFile = OS.Path.join(tmpDir, "test_move_failures_file_dest.tmp");
+ const srcDir = OS.Path.join(tmpDir, "test_move_failure_src.tmp.d");
+ await createFile(destFile);
+ await createDir(srcDir);
+ // Test.
+ await Assert.rejects(
+ IOUtils.move(srcDir, destFile),
+ /Could not move the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/,
+ "IOUtils::move throws if try to move dir into an existing file"
+ );
+
+ // Clean up.
+ await cleanup(destFile, srcDir);
+ });
+
+ add_task(async function test_copy() {
+ // Set up.
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_orig.tmp");
+ const destFileName = OS.Path.join(tmpDir, "test_ioutils_copy.tmp");
+ await createFile(tmpFileName, "original");
+ // Test.
+ info("Test copy to new file in same directory");
+ await IOUtils.copy(tmpFileName, destFileName);
+ ok(
+ await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "original"),
+ "IOUtils::copy can copy source to dest in same directory"
+ );
+
+ // Set up.
+ info("Test copy to existing file with no overwrite");
+ await createFile(tmpFileName, "new contents");
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(tmpFileName, destFileName, { noOverwrite: true }),
+ /Could not copy source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/,
+ "IOUtils::copy will refuse to copy to existing destination if overwrites are disabled"
+ );
+ ok(
+ await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "original"),
+ "Failed IOUtils::move doesn't move the source file"
+ );
+
+ // Test.
+ info("Test copy to existing file with overwrite");
+ await IOUtils.copy(tmpFileName, destFileName, { noOverwrite: false });
+ ok(await fileExists(tmpFileName), "IOUtils::copy retains source");
+ ok(
+ await fileHasTextContents(destFileName, "new contents"),
+ "IOUtils::copy overwrote the destination with the source"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName, destFileName);
+ });
+
+ add_task(async function test_copy_file_to_dir() {
+ // Set up.
+ info("Test copy file to non-existing directory");
+ const tmpFileName = OS.Path.join(tmpDir, "test_copy_file_to_dir.tmp");
+ const destDir = OS.Path.join(tmpDir, "test_copy_file_to_dir.tmp.d");
+ const dest = OS.Path.join(destDir, "dest.tmp");
+ await createFile(tmpFileName);
+ // Test.
+ ok(!await IOUtils.exists(destDir), "Expected path not to exist");
+ await IOUtils.copy(tmpFileName, dest);
+ ok(
+ await fileExists(tmpFileName) && await fileExists(dest),
+ "IOUtils::copy creates non-existing parents if needed"
+ );
+
+ // Set up.
+ info("Test copy file to existing directory")
+ await createFile(tmpFileName);
+ // Test.
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ await IOUtils.copy(tmpFileName, dest);
+ ok(
+ await fileExists(tmpFileName)
+ && await fileExists(dest),
+ "IOUtils::copy can copy a file into an existing dir"
+ );
+
+ // Set up.
+ info("Test copy file to existing directory without specifying leaf name")
+ await createFile(tmpFileName);
+ // Test.
+ await IOUtils.copy(tmpFileName, destDir);
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ ok(
+ await fileExists(tmpFileName)
+ && await fileExists(OS.Path.join(destDir, OS.Path.basename(tmpFileName))),
+ "IOUtils::copy can copy a file into an existing dir"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName, destDir);
+ });
+
+ add_task(async function test_copy_dir_recursive() {
+ // Set up.
+ info("Test rename an empty directory");
+ const srcDir = OS.Path.join(tmpDir, "test_copy_dir.tmp.d");
+ const destDir = OS.Path.join(tmpDir, "test_copy_dir_dest.tmp.d");
+ await createDir(srcDir);
+ // Test.
+ await IOUtils.copy(srcDir, destDir, { recursive: true });
+ ok(
+ await dirExists(srcDir) && await dirExists(destDir),
+ "IOUtils::copy can recursively copy entire directories"
+ );
+
+ // Set up.
+ info("Test copy directory and its content into another directory");
+ await createDir(srcDir);
+ await createFile(OS.Path.join(srcDir, "file.tmp"), "foo");
+ // Test.
+ await IOUtils.copy(srcDir, destDir, { recursive: true });
+ const destFile = OS.Path.join(destDir, OS.Path.basename(srcDir), "file.tmp");
+ ok(
+ await dirExists(srcDir)
+ && await dirExists(destDir)
+ && await dirExists(OS.Path.join(destDir, OS.Path.basename(srcDir)))
+ && await fileHasTextContents(destFile, "foo"),
+ "IOUtils::copy can move a directory and its contents into another one"
+ )
+
+ // Clean up.
+ await cleanup(srcDir, destDir);
+ });
+
+ add_task(async function test_copy_failures() {
+ // Set up.
+ info("Test attempt to copy a non-existent source file");
+ const notExistsSrc = OS.Path.join(tmpDir, "test_copy_not_exists_src.tmp");
+ const notExistsDest = OS.Path.join(tmpDir, "test_copy_not_exists_dest.tmp");
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(notExistsSrc, notExistsDest),
+ /Could not copy source file\(.*\) because it does not exist/,
+ "IOUtils::copy throws if source file does not exist"
+ );
+ ok(
+ !await fileExists(notExistsSrc) && !await fileExists(notExistsDest),
+ "IOUtils::copy failure due to missing source file does not affect destination"
+ );
+
+ // Set up.
+ info("Test attempt to copy a directory to a file");
+ const destFile = OS.Path.join(tmpDir, "test_copy_failures_file_dest.tmp");
+ const srcDir = OS.Path.join(tmpDir, "test_copy_failure_src.tmp.d");
+ await createFile(destFile);
+ await createDir(srcDir);
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(srcDir, destFile, { recursive: true }),
+ /Could not copy the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/,
+ "IOUtils::copy throws if try to move dir into an existing file"
+ );
+ ok(await fileHasTextContents(destFile, ""), "IOUtils::copy failure does not affect destination");
+
+ // Set up.
+ info("Test copy directory without recursive option");
+ await createDir(srcDir);
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(srcDir, notExistsDest, { recursive: false }),
+ /Refused to copy source directory\(.*\) to the destination\(.*\)/,
+ "IOUtils::copy throws if try to copy a directory with { recursive: false }"
+ );
+ console.log(`${notExistsDest} exists?`, await IOUtils.exists(notExistsDest))
+ ok(!await IOUtils.exists(notExistsDest), "IOUtils::copy failure does not affect destination");
+
+ // Clean up.
+ await cleanup(destFile, srcDir);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_dir_iteration.html b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html
new file mode 100644
index 0000000000..2f1181fa23
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html
@@ -0,0 +1,84 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+ const tmpDir = OS.Constants.Path.tmpDir;
+
+ add_task(async function iterate_dir_failure() {
+ let notExists = OS.Path.join(tmpDir, 'does_not_exist_dir.tmp.d');
+
+ await Assert.rejects(
+ IOUtils.getChildren(notExists),
+ /Could not get children of file\(.*\) because it does not exist/,
+ "IOUtils::getChildren rejects if the file does not exist"
+ );
+ ok(!await fileExists(notExists), `Expected ${notExists} not to exist`);
+
+ info('Try to get the children of a regular file');
+
+ let tmpFileName = OS.Path.join(tmpDir, 'iterator_file.tmp');
+ await createFile(tmpFileName)
+ await Assert.rejects(IOUtils.getChildren(tmpFileName),
+ /Could not get children of file\(.*\) because it is not a directory/,
+ "IOUtils::getChildren rejects if the file is not a dir"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function iterate_dir() {
+ info('Try to get the children of a multi-level directory hierarchy');
+
+ let root = OS.Path.join(tmpDir, 'iterator.tmp.d');
+ let child1 = OS.Path.join(root, 'child1.tmp');
+ let child2 = OS.Path.join(root, 'child2.tmp');
+ let grandchild = OS.Path.join(child1, 'grandchild.tmp');
+
+ await createDir(grandchild); // Ancestors will be created.
+ await createDir(child2);
+
+ let entries = await IOUtils.getChildren(root);
+
+ is(entries.length, 2, `Expected 2 entries below the path at ${root}`);
+ ok(!entries.includes(grandchild), "IOUtils::getChildren does not enter subdirectories");
+
+ await cleanup(root);
+ });
+
+ add_task(async function iterate_empty_dir() {
+ info('Try to get the children of an empty directory');
+
+ let emptyDir = OS.Path.join(tmpDir, 'iterator_empty_dir.tmp.d');
+ await createDir(emptyDir);
+
+ is(
+ (await IOUtils.getChildren(emptyDir)).length,
+ 0,
+ "IOUtils::getChildren return an empty array when called on an empty dir"
+ );
+
+ await cleanup(emptyDir);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_mkdir.html b/dom/system/tests/ioutils/test_ioutils_mkdir.html
new file mode 100644
index 0000000000..2439443e87
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html
@@ -0,0 +1,113 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+
+ add_task(async function test_make_directory() {
+ info("Test creating a new directory");
+ const tmpDir = await PathUtils.getTempDir();
+ const newDirectoryName = PathUtils.join(tmpDir, "test_ioutils_new_dir.tmp.d");
+ await IOUtils.makeDirectory(newDirectoryName);
+ ok(
+ await IOUtils.exists(newDirectoryName),
+ "IOUtils::makeDirectory can create a new directory"
+ );
+
+ info("Test creating an existing directory");
+ await IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: true });
+ ok(
+ await IOUtils.exists(newDirectoryName),
+ "IOUtils::makeDirectory can ignore existing directories"
+ );
+ await Assert.rejects(
+ IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: false }),
+ /Could not create directory because it already exists at .*/,
+ "IOUtils::makeDirectory can throw if the target dir exists"
+ )
+
+ info("Test creating a nested directory");
+ const parentDirName = PathUtils.join(tmpDir, "test_ioutils_mkdir_parent.tmp.d");
+ const nestedDirName = PathUtils.join(
+ parentDirName,
+ "test_ioutils_mkdir_child.tmp.d"
+ );
+ await Assert.rejects(
+ IOUtils.makeDirectory(nestedDirName, { createAncestors: false }),
+ /Could not create directory at .* because the path has missing ancestor components/,
+ "IOUtils::makeDirectory can fail if the target is missing parents"
+ );
+ ok(!await IOUtils.exists(nestedDirName), `Expected ${nestedDirName} not to exist`);
+ await IOUtils.makeDirectory(nestedDirName, { createAncestors: true });
+ ok(
+ await IOUtils.exists(nestedDirName),
+ "IOUtils::makeDirectory can create ancestors of the target directory"
+ );
+
+ await cleanup(newDirectoryName, parentDirName);
+ });
+
+ add_task(async function test_make_directory_failure() {
+ info("Try to create a directory where a file already exists");
+ const tmpDir = await PathUtils.getTempDir();
+ const notADirFileName = PathUtils.join(tmpDir, "test_ioutils_not_a_dir.tmp");
+ await createFile(notADirFileName);
+
+ await Assert.rejects(
+ IOUtils.makeDirectory(notADirFileName, { ignoreExisting: false }),
+ /Could not create directory because the target file\(.*\) exists and is not a directory/,
+ "IOUtils::makeDirectory [ignoreExisting: false] throws when the target is an existing file"
+ );
+ ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`);
+
+ await Assert.rejects(
+ IOUtils.makeDirectory(notADirFileName, { ignoreExisting: true }),
+ /Could not create directory because the target file\(.*\) exists and is not a directory/,
+ "IOUtils::makeDirectory [ignoreExisting: true] throws when the target is an existing file"
+ );
+ ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`);
+
+ await cleanup(notADirFileName);
+ });
+
+ add_task(async function test_make_directory_permissions() {
+ if (Services.appinfo.OS === "WINNT") {
+ ok(true, "Skipping test on unsupported platform (Windows)");
+ return;
+ }
+
+ const tmpDir = await PathUtils.getTempDir();
+ const newDir = PathUtils.join(tmpDir, "test_ioutils_mkdir_perms.tmp.d");
+
+ ok(!await IOUtils.exists(newDir), "Directory does not exist before creation");
+ await IOUtils.makeDirectory(newDir, { permissions: 0o751 });
+ ok(await IOUtils.exists(newDir), "Directory created");
+
+ const stat = await IOUtils.stat(newDir);
+ is(stat.type, "directory", "Directory stat() as directory");
+ is(stat.permissions, 0o751, "Directory created with expected permissions");
+
+ await cleanup(newDir);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_read_write.html b/dom/system/tests/ioutils/test_ioutils_read_write.html
new file mode 100644
index 0000000000..f888f03449
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write.html
@@ -0,0 +1,432 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ // This is presently only used to test compatability between OS.File and
+ // IOUtils when it comes to writing compressed files. The import and the
+ // test `test_lz4_osfile_compat` can be removed with OS.File is removed.
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+ add_task(async function test_read_failure() {
+ const tmpDir = await PathUtils.getTempDir();
+ const doesNotExist = PathUtils.join(tmpDir, "does_not_exist.tmp");
+ await Assert.rejects(
+ IOUtils.read(doesNotExist),
+ /Could not open the file at .*/,
+ "IOUtils::read rejects when file does not exist"
+ );
+ });
+
+ add_task(async function test_write_no_overwrite() {
+ const tmpDir = await PathUtils.getTempDir();
+
+ // Make a new file, and try to write to it with overwrites disabled.
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_overwrite.tmp");
+ const untouchableContents = new TextEncoder().encode("Can't touch this!\n");
+
+ let exists = await IOUtils.exists(tmpFileName);
+ ok(!exists, `File ${tmpFileName} should not exist before writing`);
+
+ await IOUtils.write(tmpFileName, untouchableContents);
+
+ exists = await IOUtils.exists(tmpFileName);
+ ok(exists, `File ${tmpFileName} should exist after writing`);
+
+ const newContents = new TextEncoder().encode("Nah nah nah!\n");
+ await Assert.rejects(
+ IOUtils.write(tmpFileName, newContents, {
+ noOverwrite: true,
+ }),
+ /Refusing to overwrite the file at */,
+ "IOUtils::write rejects writing to existing file if overwrites are disabled"
+ );
+ ok(
+ await fileHasBinaryContents(tmpFileName, untouchableContents),
+ "IOUtils::write doesn't change target file when overwrite is refused"
+ );
+
+ const bytesWritten = await IOUtils.write(
+ tmpFileName,
+ newContents,
+ { noOverwrite: false /* Default. */ }
+ );
+ is(
+ bytesWritten,
+ newContents.length,
+ "IOUtils::write can overwrite files if specified"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_with_backup() {
+ info("Test backup file option with non-existing file");
+
+ const tmpDir = await PathUtils.getTempDir();
+
+ let fileContents = new TextEncoder().encode("Original file contents");
+ let destFileName = PathUtils.join(tmpDir, "test_write_with_backup_option.tmp");
+ let backupFileName = destFileName + ".backup";
+ let bytesWritten =
+ await IOUtils.write(destFileName, fileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::write creates a new file with the correct contents"
+ );
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::write does not create a backup if the target file does not exist"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::write correctly writes to a new file without performing a backup"
+ );
+
+ info("Test backup file option with existing destination");
+ let newFileContents = new TextEncoder().encode("New file contents");
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ bytesWritten =
+ await IOUtils.write(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::write can backup an existing file before writing"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::write can create the target with the correct contents"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::write correctly writes to the target after taking a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_write_with_backup_and_tmp() {
+ info("Test backup with tmp and backup file options, non-existing destination");
+
+ const tmpDir = await PathUtils.getTempDir();
+
+ let fileContents = new TextEncoder().encode("Original file contents");
+ let destFileName = PathUtils.join(tmpDir, "test_write_with_backup_and_tmp_options.tmp");
+ let backupFileName = destFileName + ".backup";
+ let tmpFileName = PathUtils.join(tmpDir, "temp_file.tmp");
+ let bytesWritten =
+ await IOUtils.write(destFileName, fileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+ ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::write does not create a backup if the target file does not exist"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::write can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::write can copy tmp file to destination without performing a backup"
+ );
+
+ info("Test backup with tmp and backup file options, existing destination");
+ let newFileContents = new TextEncoder().encode("New file contents");
+ bytesWritten =
+ await IOUtils.write(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+
+ ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::write can create a backup if the target file exists"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::write can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_partial_read() {
+ const tmpDir = await PathUtils.getTempDir();
+
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_partial_read.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ const bytesWritten = await IOUtils.write(tmpFileName, bytes);
+ is(
+ bytesWritten,
+ 50,
+ "IOUtils::write can write entire byte array to file"
+ );
+
+ // Read just the first 10 bytes.
+ const first10 = bytes.slice(0, 10);
+ const bytes10 = await IOUtils.read(tmpFileName, { maxBytes: 10 });
+ ok(
+ ObjectUtils.deepEqual(bytes10, first10),
+ "IOUtils::read can read part of a file, up to specified max bytes"
+ );
+
+ // Trying to explicitly read nothing isn't useful, but it should still
+ // succeed.
+ const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 });
+ is(bytes0.length, 0, "IOUtils::read can read 0 bytes");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_empty_read_and_write() {
+ // Trying to write an empty file isn't very useful, but it should still
+ // succeed.
+
+ const tmpDir = await PathUtils.getTempDir();
+
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_empty.tmp");
+ const emptyByteArray = new Uint8Array(0);
+ const bytesWritten = await IOUtils.write(
+ tmpFileName,
+ emptyByteArray
+ );
+ is(bytesWritten, 0, "IOUtils::write can create an empty file");
+
+ // Trying to explicitly read nothing isn't useful, but it should still
+ // succeed.
+ const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 });
+ is(bytes0.length, 0, "IOUtils::read can read 0 bytes");
+
+ // Implicitly try to read nothing.
+ const nothing = await IOUtils.read(tmpFileName);
+ is(nothing.length, 0, "IOUtils:: read can read empty files");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_full_read_and_write() {
+ // Write a file.
+
+ const tmpDir = await PathUtils.getTempDir();
+
+ info("Test writing to a new binary file");
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_numbers.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ const bytesWritten = await IOUtils.write(tmpFileName, bytes);
+ is(
+ bytesWritten,
+ 50,
+ "IOUtils::write can write entire byte array to file"
+ );
+
+ // Read it back.
+ info("Test reading a binary file");
+ let fileContents = await IOUtils.read(tmpFileName);
+ ok(
+ ObjectUtils.deepEqual(bytes, fileContents) &&
+ bytes.length == fileContents.length,
+ "IOUtils::read can read back entire file"
+ );
+
+ const tooManyBytes = bytes.length + 1;
+ fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes });
+ ok(
+ ObjectUtils.deepEqual(bytes, fileContents) &&
+ fileContents.length == bytes.length,
+ "IOUtils::read can read entire file when requested maxBytes is too large"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_relative_path() {
+ const tmpFileName = "test_ioutils_write_relative_path.tmp";
+ const bytes = Uint8Array.of(...new Array(50).keys());
+
+ info("Test writing a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.write(tmpFileName, bytes),
+ /Could not parse path/,
+ "IOUtils::write only works with absolute paths"
+ );
+ });
+
+ add_task(async function test_read_relative_path() {
+ const tmpFileName = "test_ioutils_read_relative_path.tmp";
+
+ info("Test reading a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.read(tmpFileName),
+ /Could not parse path/,
+ "IOUtils::write only works with absolute paths"
+ );
+ });
+
+ add_task(async function test_lz4() {
+ const tmpDir = await PathUtils.getTempDir();
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_lz4.tmp");
+
+ info("Test writing lz4 encoded data");
+ const varyingBytes = Uint8Array.of(...new Array(50).keys());
+ let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true });
+ is(bytesWritten, 64, "Expected to write 64 bytes");
+
+ info("Test reading lz4 encoded data");
+ let readData = await IOUtils.read(tmpFileName, { decompress: true });
+ ok(readData.equals(varyingBytes), "IOUtils can write and read back LZ4 encoded data");
+
+ info("Test writing lz4 compressed data");
+ const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
+ bytesWritten = await IOUtils.write(tmpFileName, repeatedBytes, { compress: true });
+ is(bytesWritten, 23, "Expected 50 bytes to compress to 23 bytes");
+
+ info("Test reading lz4 encoded data");
+ readData = await IOUtils.read(tmpFileName, { decompress: true });
+ ok(readData.equals(repeatedBytes), "IOUtils can write and read back LZ4 compressed data");
+
+ info("Test writing empty lz4 compressed data")
+ const empty = new Uint8Array();
+ bytesWritten = await IOUtils.write(tmpFileName, empty, { compress: true });
+ is(bytesWritten, 12, "Expected to write just the LZ4 header, with a content length of 0");
+
+
+ info("Test reading empty lz4 compressed data")
+ const readEmpty = await IOUtils.read(tmpFileName, { decompress: true });
+ ok(readEmpty.equals(empty), "IOUtils can write and read back empty buffers with LZ4");
+ const readEmptyRaw = await IOUtils.read(tmpFileName, { decompress: false });
+ is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header");
+ const expectedHeader = Uint8Array.of(109, 111, 122, 76, 122, 52, 48, 0, 0, 0, 0, 0); // "mozLz40\0\0\0\0"
+ ok(readEmptyRaw.equals(expectedHeader), "Expected to read header with content length of 0");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_lz4_osfile_compat() {
+ const tmpDir = await PathUtils.getTempDir();
+ const osfileTmpFile = PathUtils.join(tmpDir, "test_ioutils_lz4_compat_osfile.tmp");
+ const ioutilsTmpFile = PathUtils.join(tmpDir, "test_ioutils_lz4_compat_ioutils.tmp");
+
+ info("Test OS.File and IOUtils write the same file with LZ4 compression enabled")
+ const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
+ let expectedBytes = 23;
+ let ioutilsBytes = await IOUtils.write(ioutilsTmpFile, repeatedBytes, { compress: true });
+ let osfileBytes = await OS.File.writeAtomic(osfileTmpFile, repeatedBytes, { compression: "lz4" });
+ is(ioutilsBytes, expectedBytes, "IOUtils writes the expected number of bytes for compression");
+ is(osfileBytes, ioutilsBytes, "OS.File and IOUtils write the same number of bytes for LZ4 compression");
+
+ info("Test OS.File can read a file compressed by IOUtils");
+ const osfileReadBytes = await OS.File.read(ioutilsTmpFile, { compression: "lz4" });
+ ok(osfileReadBytes.every(byte => byte === 1), "OS.File can read a file compressed by IOUtils");
+ is(osfileReadBytes.length, 50, "OS.File reads the right number of bytes from a file compressed by IOUtils")
+
+ info("Test IOUtils can read a file compressed by OS.File");
+ const ioutilsReadBytes = await IOUtils.read(osfileTmpFile, { decompress: true });
+ ok(ioutilsReadBytes.every(byte => byte === 1), "IOUtils can read a file compressed by OS.File");
+ is(ioutilsReadBytes.length, 50, "IOUtils reads the right number of bytes from a file compressed by OS.File")
+
+ await cleanup(osfileTmpFile, ioutilsTmpFile);
+ });
+
+ add_task(async function test_lz4_bad_call() {
+ const tmpDir = await PathUtils.getTempDir();
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_lz4_bad_call.tmp");
+
+ info("Test decompression with invalid options");
+ const varyingBytes = Uint8Array.of(...new Array(50).keys());
+ let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true });
+ is(bytesWritten, 64, "Expected to write 64 bytes");
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { maxBytes: 4, decompress: true }),
+ /The `maxBytes` and `decompress` options are not compatible/,
+ "IOUtils::read rejects when maxBytes and decompress options are both used"
+ );
+
+ await cleanup(tmpFileName)
+ });
+
+ add_task(async function test_lz4_failure() {
+ const tmpDir = await PathUtils.getTempDir();
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_lz4_fail.tmp");
+
+ info("Test decompression of non-lz4 data");
+ const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
+ await IOUtils.write(tmpFileName, repeatedBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { decompress: true }),
+ (actual) => {
+ is(actual.constructor, DOMException,
+ "rejection reason constructor for decompress with bad header");
+ is(actual.name, "NotReadableError",
+ "rejection error name for decompress with bad header");
+ ok(/Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/
+ .test(actual.message),
+ "rejection error message for decompress with bad header. Got "
+ + actual.message);
+ return true;
+ },
+ "IOUtils::read fails to decompress LZ4 data with a bad header"
+ );
+
+ info("Test decompression of short byte buffer");
+ const elevenBytes = Uint8Array.of(...new Array(11).fill(1));
+ await IOUtils.write(tmpFileName, elevenBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { decompress: true }),
+ /Could not decompress file because the buffer is too short/,
+ "IOUtils::read fails to decompress LZ4 data with missing header"
+ );
+
+ info("Test decompression of valid header, but corrupt contents");
+ const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length
+ const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream.
+ const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents);
+ await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { decompress: true }),
+ /Could not decompress file contents, the file may be corrupt/,
+ "IOUtils::read fails to read corrupt LZ4 contents with a correct header"
+ );
+
+ await cleanup(tmpFileName);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_json.html b/dom/system/tests/ioutils/test_ioutils_read_write_json.html
new file mode 100644
index 0000000000..01d2771888
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html
@@ -0,0 +1,153 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import(
+ "resource://gre/modules/ObjectUtils.jsm"
+ );
+
+ const OBJECT = {
+ "foo": [
+ "bar",
+ 123,
+ 456.789,
+ true,
+ false,
+ null,
+ ],
+ "bar": {
+ "baz": {},
+ },
+ };
+
+ const ARRAY = [1, 2.3, true, false, null, { "foo": "bar" }];
+
+ const PRIMITIVES = [123, true, false, "hello, world", null];
+
+ add_task(async function read_json() {
+ const tmpDir = await PathUtils.getTempDir();
+ const filename = PathUtils.join(tmpDir, "test_ioutils_read_json.tmp");
+
+ info("Testing IOUtils.readJSON() with a serialized object...");
+ await IOUtils.writeUTF8(filename, JSON.stringify(OBJECT));
+ const readObject = await IOUtils.readJSON(filename);
+ const parsedObject = JSON.parse(await IOUtils.readUTF8(filename));
+ ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip");
+ ok(
+ ObjectUtils.deepEqual(parsedObject, readObject),
+ "IOUtils.readJSON() equivalent to JSON.parse() for objects"
+ );
+
+ info("Testing IOUtils.readJSON() with a serialized array...");
+ await IOUtils.writeUTF8(filename, JSON.stringify(ARRAY));
+ const readArray = await IOUtils.readJSON(filename);
+ const parsedArray = JSON.parse(await IOUtils.readUTF8(filename));
+ ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip");
+ ok(
+ ObjectUtils.deepEqual(parsedArray, readArray),
+ "IOUtils.readJSON() equivalent to JSON.parse(IOUtils.readUTF8()) for arrays"
+ );
+
+ info("Testing IOUtils.readJSON() with serialized primitives...");
+ for (const primitive of PRIMITIVES) {
+ await IOUtils.writeUTF8(filename, JSON.stringify(primitive));
+ const readPrimitive = await IOUtils.readJSON(filename);
+ const parsedPrimitive = JSON.parse(await IOUtils.readUTF8(filename));
+ ok(primitive === readPrimitive, `JSON primitive ${primitive} should round trip`);
+ ok(
+ readPrimitive === parsedPrimitive,
+ `${readPrimitive} === ${parsedPrimitive} -- IOUtils.readJSON() equivalent to JSON.parse() for primitive`
+ );
+ }
+
+ info("Testing IOUtils.readJSON() with a file that does not exist...");
+ const notExistsFilename = PathUtils.join(tmpDir, "test_ioutils_read_json_not_exists.tmp");
+ ok(!await IOUtils.exists(notExistsFilename), `${notExistsFilename} should not exist`);
+ await Assert.rejects(
+ IOUtils.readJSON(notExistsFilename),
+ /NotFoundError: Could not open the file at/,
+ "IOUtils::readJSON rejects when file does not exist"
+ );
+
+ info("Testing IOUtils.readJSON() with a file that does not contain JSON");
+ const invalidFilename = PathUtils.join(tmpDir, "test_ioutils_read_json_invalid.tmp");
+ await IOUtils.writeUTF8(invalidFilename, ":)");
+
+ await Assert.rejects(
+ IOUtils.readJSON(invalidFilename),
+ /SyntaxError: JSON\.parse/,
+ "IOUTils::readJSON rejects when the file contains invalid JSON"
+ );
+
+ await cleanup(filename, invalidFilename);
+ });
+
+ add_task(async function write_json() {
+ const tmpDir = await PathUtils.getTempDir();
+ const filename = PathUtils.join(tmpDir, "test_ioutils_write_json.tmp");
+
+ info("Testing IOUtils.writeJSON() with an object...");
+ await IOUtils.writeJSON(filename, OBJECT);
+ const readObject = await IOUtils.readJSON(filename);
+ const readObjectStr = await IOUtils.readUTF8(filename);
+ ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip");
+ ok(
+ readObjectStr === JSON.stringify(OBJECT),
+ "IOUtils.writeJSON() eqvuialent to JSON.stringify() for an object"
+ );
+
+ info("Testing IOUtils.writeJSON() with an array...");
+ await IOUtils.writeJSON(filename, ARRAY);
+ const readArray = await IOUtils.readJSON(filename);
+ const readArrayStr = await IOUtils.readUTF8(filename);
+ ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip");
+ ok(
+ readArrayStr === JSON.stringify(ARRAY),
+ "IOUtils.writeJSON() equivalent to JSON.stringify() for an array"
+ );
+
+ info("Testing IOUtils.writeJSON() with primitives...");
+ for (const primitive of PRIMITIVES) {
+ await IOUtils.writeJSON(filename, primitive);
+ const readPrimitive = await IOUtils.readJSON(filename);
+ const readPrimitiveStr = await IOUtils.readUTF8(filename);
+ ok(
+ primitive === readPrimitive,
+ `${primitive} === ${readPrimitive} -- IOUtils.writeJSON() should round trip primitive`
+ );
+ ok(
+ readPrimitiveStr === JSON.stringify(primitive),
+ `${readPrimitiveStr} === ${JSON.stringify(primitive)} -- IOUtils.writeJSON() equivalent to JSON.stringify for primitive`
+ );
+ }
+
+ info("Testing IOUtils.writeJSON() with unserializable objects...");
+ await Assert.rejects(
+ IOUtils.writeJSON(filename, window),
+ /TypeError: cyclic object value/,
+ "IOUtils.writeJSON() cannot write cyclic objects"
+ );
+
+ await cleanup(filename);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
new file mode 100644
index 0000000000..80a384a21e
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
@@ -0,0 +1,389 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ // TODO: Remove this import for OS.File. It is currently being used as a
+ // stop gap for missing IOUtils functionality.
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+
+ const tmpDir = OS.Constants.Path.tmpDir;
+
+ // This is an impossible sequence of bytes in an UTF-8 encoded file.
+ // See section 3.5.3 of this text:
+ // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+ const invalidUTF8 = Uint8Array.of(0xfe, 0xfe, 0xff, 0xff);
+
+ add_task(async function test_read_utf8_failure() {
+ info("Test attempt to read non-existent file (UTF8)");
+ const doesNotExist = OS.Path.join(tmpDir, "does_not_exist.tmp");
+ await Assert.rejects(
+ IOUtils.readUTF8(doesNotExist),
+ /Could not open the file at .*/,
+ "IOUtils::readUTF8 rejects when file does not exist"
+ );
+
+ info("Test attempt to read invalid UTF-8");
+ const invalidUTF8File = OS.Path.join(tmpDir, "invalid_utf8.tmp");
+
+ // Deliberately write the invalid byte sequence to file.
+ await IOUtils.write(invalidUTF8File, invalidUTF8);
+
+ await Assert.rejects(
+ IOUtils.readUTF8(invalidUTF8File),
+ /Could not read file\(.*\) because it is not UTF-8 encoded/,
+ "IOUtils::readUTF8 will reject when reading a file that is not valid UTF-8"
+ );
+
+ await cleanup(invalidUTF8File);
+ });
+
+ add_task(async function test_write_utf8_no_overwrite() {
+ // Make a new file, and try to write to it with overwrites disabled.
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_write_utf8_overwrite.tmp");
+ const untouchableContents = "Can't touch this!\n";
+ await IOUtils.writeUTF8(tmpFileName, untouchableContents);
+
+ const newContents = "Nah nah nah!\n";
+ await Assert.rejects(
+ IOUtils.writeUTF8(tmpFileName, newContents, {
+ noOverwrite: true,
+ }),
+ /Refusing to overwrite the file at */,
+ "IOUtils::writeUTF8 rejects writing to existing file if overwrites are disabled"
+ );
+ ok(
+ await fileHasTextContents(tmpFileName, untouchableContents),
+ "IOUtils::writeUTF8 doesn't change target file when overwrite is refused"
+ );
+
+ const bytesWritten = await IOUtils.writeUTF8(
+ tmpFileName,
+ newContents,
+ { noOverwrite: false /* Default. */ }
+ );
+ is(
+ bytesWritten,
+ newContents.length,
+ "IOUtils::writeUTF8 can overwrite files if specified"
+ );
+ ok(
+ await fileHasTextContents(tmpFileName, newContents),
+ "IOUtils::writeUTF8 overwrites with the expected contents"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_with_backup() {
+ info("Test backup file option with non-existing file");
+ let fileContents = "Original file contents";
+ let destFileName = OS.Path.join(tmpDir, "test_write_utf8_with_backup_option.tmp");
+ let backupFileName = destFileName + ".backup";
+ let bytesWritten =
+ await IOUtils.writeUTF8(destFileName, fileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::writeUTF8 creates a new file with the correct contents"
+ );
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::writeUTF8 does not create a backup if the target file does not exist"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::write correctly writes to a new file without performing a backup"
+ );
+
+ info("Test backup file option with existing destination");
+ let newFileContents = "New file contents";
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ bytesWritten =
+ await IOUtils.writeUTF8(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::writeUTF8 can backup an existing file before writing"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::writeUTF8 can create the target with the correct contents"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::writeUTF8 correctly writes to the target after taking a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_write_with_backup_and_tmp() {
+ info("Test backup with tmp and backup file options, non-existing destination");
+ let fileContents = "Original file contents";
+ let destFileName = OS.Path.join(tmpDir, "test_write_utf8_with_backup_and_tmp_options.tmp");
+ let backupFileName = destFileName + ".backup";
+ let tmpFileName = OS.Path.join(tmpDir, "temp_file.tmp");
+ let bytesWritten =
+ await IOUtils.writeUTF8(destFileName, fileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+ ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile");
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::writeUTF8 does not create a backup if the target file does not exist"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::writeUTF8 can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::writeUTF8 can copy tmp file to destination without performing a backup"
+ );
+
+ info("Test backup with tmp and backup file options, existing destination");
+ let newFileContents = "New file contents";
+ bytesWritten =
+ await IOUtils.writeUTF8(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+
+ ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile");
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::writeUTF8 can create a backup if the target file exists"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::writeUTF8 can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::writeUTF8 can move tmp file to destination after performing a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_empty_read_and_write_utf8() {
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_empty_utf8.tmp");
+ const emptyString = ""
+ const bytesWritten = await IOUtils.writeUTF8(
+ tmpFileName,
+ emptyString
+ );
+ is(bytesWritten, 0, "IOUtils::writeUTF8 can create an empty file");
+
+ const nothing = await IOUtils.readUTF8(tmpFileName);
+ is(nothing.length, 0, "IOUtils::readUTF8 can read empty files");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_full_read_and_write_utf8() {
+ // Write a file.
+ info("Test writing emoji file");
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_emoji.tmp");
+
+ // Make sure non-ASCII text is supported for writing and reading back.
+ // For fun, a sampling of space-separated emoji characters from different
+ // Unicode versions, including multi-byte glyphs that are rendered using
+ // ZWJ sequences.
+ const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
+ const expectedBytes = 71;
+ const bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji);
+ is(
+ bytesWritten,
+ expectedBytes,
+ "IOUtils::writeUTF8 can write emoji to file"
+ );
+
+ // Read it back.
+ info("Test reading emoji from file");
+ let fileContents = await IOUtils.readUTF8(tmpFileName);
+ ok(
+ emoji == fileContents &&
+ emoji.length == fileContents.length,
+ "IOUtils::readUTF8 can read back entire file"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_utf8_relative_path() {
+ const tmpFileName = "test_ioutils_write_utf8_relative_path.tmp";
+
+ info("Test writing a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.writeUTF8(tmpFileName, "foo"),
+ /Could not parse path/,
+ "IOUtils::writeUTF8 only works with absolute paths"
+ );
+ });
+
+ add_task(async function test_read_utf8_relative_path() {
+ const tmpFileName = "test_ioutils_read_utf8_relative_path.tmp";
+
+ info("Test reading a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName),
+ /Could not parse path/,
+ "IOUtils::readUTF8 only works with absolute paths"
+ );
+ });
+
+
+ add_task(async function test_utf8_lz4() {
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4.tmp");
+
+ info("Test writing lz4 encoded UTF-8 string");
+ const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
+ let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true });
+ is(bytesWritten, 83, "Expected to write 64 bytes");
+
+ info("Test reading lz4 encoded UTF-8 string");
+ let readData = await IOUtils.readUTF8(tmpFileName, { decompress: true });
+ is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data");
+
+ info("Test writing lz4 compressed UTF-8 string");
+ const lotsOfCoffee = new Array(24).fill("☕️").join(""); // ☕️ is 3 bytes in UTF-8: \0xe2 \0x98 \0x95
+ bytesWritten = await IOUtils.writeUTF8(tmpFileName, lotsOfCoffee, { compress: true });
+ console.log(bytesWritten);
+ is(bytesWritten, 28, "Expected 72 bytes to compress to 28 bytes");
+
+ info("Test reading lz4 encoded UTF-8 string");
+ readData = await IOUtils.readUTF8(tmpFileName, { decompress: true });
+ is(readData, lotsOfCoffee, "IOUtils can write and read back UTF-8 LZ4 compressed data");
+
+ info("Test writing empty lz4 compressed UTF-8 string")
+ const empty = "";
+ bytesWritten = await IOUtils.writeUTF8(tmpFileName, empty, { compress: true });
+ is(bytesWritten, 12, "Expected to write just the LZ4 header");
+
+ info("Test reading empty lz4 compressed UTF-8 string")
+ const readEmpty = await IOUtils.readUTF8(tmpFileName, { decompress: true });
+ is(readEmpty, empty, "IOUtils can write and read back empty buffers with LZ4");
+ const readEmptyRaw = await IOUtils.readUTF8(tmpFileName, { decompress: false });
+ is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_utf8_lz4_osfile_compat() {
+ const osfileTmpFile = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_compat_osfile.tmp");
+ const ioutilsTmpFile = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_compat_ioutils.tmp");
+
+ info("Test OS.File and IOUtils write the same UTF-8 file with LZ4 compression enabled")
+ const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
+ let expectedBytes = 83;
+ let ioutilsBytes = await IOUtils.writeUTF8(ioutilsTmpFile, emoji, { compress: true });
+ let osfileBytes = await OS.File.writeAtomic(osfileTmpFile, emoji, { compression: "lz4" });
+ is(ioutilsBytes, expectedBytes, "IOUtils writes the expected number of bytes for compression");
+ is(osfileBytes, ioutilsBytes, "OS.File and IOUtils write the same number of bytes for LZ4 compression");
+
+ info("Test OS.File can read an UTF-8 file compressed by IOUtils");
+ const osfileReadStr = await OS.File.read(ioutilsTmpFile, { compression: "lz4", encoding: "utf-8" });
+ is(osfileReadStr, emoji, "OS.File can read an UTF-8 file compressed by IOUtils")
+
+ info("Test IOUtils can read an UTF-8 file compressed by OS.File");
+ const ioutilsReadString = await IOUtils.readUTF8(ioutilsTmpFile, { decompress: true });
+ is(ioutilsReadString, emoji, "IOUtils can read an UTF-8 file compressed by OS.File");
+
+ await cleanup(osfileTmpFile, ioutilsTmpFile);
+ });
+
+ add_task(async function test_utf8_lz4_bad_call() {
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_bad_call.tmp");
+
+ info("readUTF8 ignores the maxBytes option if provided");
+ const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
+ let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true });
+ is(bytesWritten, 83, "Expected to write 83 bytes");
+
+ let readData = await IOUtils.readUTF8(tmpFileName, { maxBytes: 4, decompress: true });
+ is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data");
+
+ await cleanup(tmpFileName)
+ });
+
+ add_task(async function test_utf8_lz4_failure() {
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_utf8_lz4_fail.tmp");
+
+ info("Test decompression of non-lz4 UTF-8 string");
+ const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
+ await IOUtils.write(tmpFileName, repeatedBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/,
+ "IOUtils::readUTF8 fails to decompress LZ4 data with a bad header"
+ );
+
+ info("Test UTF-8 decompression of short byte buffer");
+ const elevenBytes = Uint8Array.of(...new Array(11).fill(1));
+ await IOUtils.write(tmpFileName, elevenBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file because the buffer is too short/,
+ "IOUtils::readUTF8 fails to decompress LZ4 data with missing header"
+ );
+
+ info("Test UTF-8 decompression of valid header, but corrupt contents");
+ const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length
+ const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream.
+ const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents);
+ await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file contents, the file may be corrupt/,
+ "IOUtils::readUTF8 fails to read corrupt LZ4 contents with a correct header"
+ );
+
+ info("Testing decompression of an empty file (no header)");
+ {
+ const n = await IOUtils.writeUTF8(tmpFileName, "");
+ ok(n === 0, "Overwrote with empty file");
+ }
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file because the buffer is too short/,
+ "IOUtils::readUTF8 fails to decompress empty files"
+ );
+
+ await cleanup(tmpFileName);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_remove.html b/dom/system/tests/ioutils/test_ioutils_remove.html
new file mode 100644
index 0000000000..b30d86654d
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_remove.html
@@ -0,0 +1,97 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ // TODO: Remove this import for OS.File. It is currently being used as a
+ // stop gap for missing IOUtils functionality.
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+
+ const tmpDir = OS.Constants.Path.tmpDir;
+
+ add_task(async function test_create_and_remove_file() {
+ info("Test creating and removing a single file");
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_create_and_remove.tmp");
+ await IOUtils.write(tmpFileName, new Uint8Array(0));
+ ok(await fileExists(tmpFileName), `Expected file ${tmpFileName} to exist`);
+
+ await IOUtils.remove(tmpFileName);
+ ok(!await fileExists(tmpFileName), "IOUtils::remove can remove files");
+
+ info("Test creating and removing an empty directory");
+ const tmpDirName = OS.Path.join(tmpDir, "test_ioutils_create_and_remove.tmp.d");
+ await IOUtils.makeDirectory(tmpDirName);
+ ok(await dirExists(tmpDirName), `Expected directory ${tmpDirName} to exist`);
+
+ await IOUtils.remove(tmpDirName);
+ ok(!await dirExists(tmpDirName), "IOUtils::remove can remove empty directories");
+ });
+
+ add_task(async function test_remove_non_existing() {
+ const tmpFileName = OS.Path.join(tmpDir, "test_ioutil_remove_non_existing.tmp");
+ ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`);
+
+ await IOUtils.remove(tmpFileName, { ignoreAbsent: true });
+ ok(!await fileExists(tmpFileName), "IOUtils::remove can ignore missing files without error");
+
+ await Assert.rejects(
+ IOUtils.remove(tmpFileName, { ignoreAbsent: false }),
+ /Could not remove the file at .* because it does not exist/,
+ "IOUtils::remove can throw an error when target file is missing"
+ );
+ ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`);
+ });
+
+ add_task(async function test_remove_recursive() {
+ const tmpParentDir = OS.Path.join(tmpDir, "test_ioutils_remove.tmp.d");
+ const tmpChildDir = OS.Path.join(tmpParentDir, "child.tmp.d");
+ const tmpTopLevelFileName = OS.Path.join(tmpParentDir, "top.tmp");
+ const tmpNestedFileName = OS.Path.join(tmpChildDir, "nested.tmp");
+ await createDir(tmpChildDir);
+ await createFile(tmpTopLevelFileName, "");
+ await createFile(tmpNestedFileName, "");
+
+ ok(
+ await fileExists(tmpTopLevelFileName),
+ `Expected file ${tmpTopLevelFileName} to exist`
+ );
+ ok(
+ await fileExists(tmpNestedFileName),
+ `Expected file ${tmpNestedFileName} to exist`
+ );
+
+ await Assert.rejects(
+ IOUtils.remove(tmpParentDir, { recursive: false }),
+ /Could not remove the non-empty directory at .*/,
+ "IOUtils::remove fails if non-recursively removing directory with contents"
+ );
+
+ await IOUtils.remove(tmpParentDir, { recursive: true });
+ ok(
+ !await dirExists(tmpParentDir),
+ "IOUtils::remove can recursively remove a directory"
+ );
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_set_permissions.html b/dom/system/tests/ioutils/test_ioutils_set_permissions.html
new file mode 100644
index 0000000000..8cf4494c5d
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_set_permissions.html
@@ -0,0 +1,56 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ add_task(async function test_setPermissions() {
+ const tempDir = await PathUtils.getTempDir();
+ const tempFile = PathUtils.join(tempDir, "setPermissions.tmp");
+
+ await IOUtils.writeUTF8(tempFile, "");
+ await IOUtils.setPermissions(tempFile, 0o421);
+
+ let stat = await IOUtils.stat(tempFile);
+
+ if (Services.appinfo.OS === "WINNT") {
+ // setPermissions ignores the x bit on Windows.
+ is(stat.permissions, 0o666, "Permissions munged on Windows");
+ } else {
+ is(stat.permissions, 0o421, "Permissions match");
+ }
+
+ await IOUtils.setPermissions(tempFile, 0o400);
+ stat = await IOUtils.stat(tempFile);
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(stat.permissions, 0o444, "Permissions munged on Windows");
+
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(tempFile, 0o600);
+ } else {
+ is(stat.permissions, 0o400, "Permissions match");
+ }
+
+ await cleanup(tempFile);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_stat_touch.html b/dom/system/tests/ioutils/test_ioutils_stat_touch.html
new file mode 100644
index 0000000000..c8b052fadb
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_stat_touch.html
@@ -0,0 +1,204 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test the IOUtils file I/O API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ add_task(async function test_stat() {
+ info("Test attempt to stat a regular empty file");
+
+ const tmpDir = await PathUtils.getTempDir();
+
+ const emptyFileName = PathUtils.join(tmpDir, "test_stat_empty.tmp");
+ await createFile(emptyFileName);
+
+ const emptyFileInfo = await IOUtils.stat(emptyFileName);
+ is(emptyFileInfo.size, 0, "IOUtils::stat can get correct (empty) file size");
+ is(emptyFileInfo.path, emptyFileName, "IOUtils::stat result contains the path");
+ is(emptyFileInfo.type, "regular", "IOUtils::stat can stat regular (empty) files");
+ Assert.less(
+ (emptyFileInfo.lastModified - new Date().valueOf()),
+ 1000, // Allow for 1 second deviation in case of slow tests.
+ "IOUtils::stat can get the last modification date for a regular file"
+ );
+
+ info("Test attempt to stat a regular binary file");
+ const tempFileName = PathUtils.join(tmpDir, "test_stat_binary.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ await createFile(tempFileName, bytes);
+
+ const fileInfo = await IOUtils.stat(tempFileName);
+ is(fileInfo.size, 50, "IOUtils::stat can get correct file size");
+ is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path");
+ is(fileInfo.type, "regular", "IOUtils::stat can stat regular files");
+ Assert.less(
+ (fileInfo.lastModified - new Date().valueOf()),
+ 1000, // Allow for 1 second deviation in case of slow tests.
+ "IOUtils::stat can get the last modification date for a regular file"
+ );
+
+ info("Test attempt to stat a directory");
+ const tempDirName = PathUtils.join(tmpDir, "test_stat_dir.tmp.d");
+ await IOUtils.makeDirectory(tempDirName);
+
+ const dirInfo = await IOUtils.stat(tempDirName);
+ is(dirInfo.size, -1, "IOUtils::stat reports -1 size for directories")
+ is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path");
+ is(fileInfo.type, "regular", "IOUtils::stat can stat directories");
+ Assert.less(
+ (fileInfo.lastModified - new Date().valueOf()),
+ 1000, // Allow for 1 second deviation in case of slow tests.
+ "IOUtils::stat can get the last modification date for a regular file"
+ );
+
+ await cleanup(emptyFileName, tempFileName, tempDirName)
+ });
+
+ add_task(async function test_stat_failures() {
+ info("Test attempt to stat a non-existing file");
+
+ const tmpDir = await PathUtils.getTempDir();
+
+ const notExistsFile = PathUtils.join(tmpDir, "test_stat_not_exists.tmp");
+
+ await Assert.rejects(
+ IOUtils.stat(notExistsFile),
+ /Could not stat file\(.*\) because it does not exist/,
+ "IOUtils::stat throws if the target file does not exist"
+ );
+ });
+
+ add_task(async function test_touch_and_stat() {
+ info("Test attempt to touch a file");
+
+ const tmpDir = await PathUtils.getTempDir();
+
+ const tmpFileName = PathUtils.join(tmpDir, "test_touch_and_stat.tmp");
+ await createFile(tmpFileName);
+
+ const oldFileInfo = await IOUtils.stat(tmpFileName);
+ await sleep(500);
+
+ // Now update the time stamp.
+ const stamp = await IOUtils.touch(tmpFileName);
+ const newFileInfo = await IOUtils.stat(tmpFileName);
+
+ ok(
+ newFileInfo.lastModified > oldFileInfo.lastModified,
+ "IOUtils::touch can update the lastModified time stamp on the file system"
+ );
+ is(
+ stamp,
+ newFileInfo.lastModified,
+ "IOUtils::touch returns the updated time stamp."
+ );
+
+ info("Test attempt to touch a directory");
+ const tmpDirName = PathUtils.join(tmpDir, "test_touch_and_stat.tmp.d");
+ await createDir(tmpDirName);
+
+ await cleanup(tmpFileName, tmpDirName);
+ });
+
+ add_task(async function test_touch_custom_mod_time() {
+ const tmpDir = await PathUtils.getTempDir();
+
+ const tempFileName = PathUtils.join(tmpDir, "test_touch_custom_mod_time.tmp");
+ await createFile(tempFileName);
+ const originalInfo = await IOUtils.stat(tempFileName);
+ const now = originalInfo.lastModified;
+
+ const oneMinute = 60 * 1000; // milliseconds
+
+ info("Test attempt to set modification time to the future");
+ const future = now + oneMinute;
+ let newModTime = await IOUtils.touch(tempFileName, future);
+ const futureInfo = await IOUtils.stat(tempFileName);
+ Assert.less(originalInfo.lastModified, futureInfo.lastModified, "IOUtils::touch can set a future modification time for the file");
+
+ is(newModTime, futureInfo.lastModified, "IOUtils::touch returns the updated time stamp");
+ is(newModTime, future, "IOUtils::touch return value matches the argument value exactly");
+
+ info("Test attempt to set modification time to the past");
+ const past = now - 2 * oneMinute;
+ newModTime = await IOUtils.touch(tempFileName, past);
+ const pastInfo = await IOUtils.stat(tempFileName);
+ Assert.greater(originalInfo.lastModified, pastInfo.lastModified, "IOUtils::touch can set a past modification time for the file");
+
+ is(newModTime, pastInfo.lastModified, "IOUtils::touch returns the updated time stamp");
+ is(newModTime, past, "IOUtils::touch return value matches the argument value exactly");
+
+ await cleanup(tempFileName);
+ });
+
+ add_task(async function test_stat_btime() {
+ if (["Darwin", "WINNT"].includes(Services.appinfo.OS)) {
+ const tmpDir = await PathUtils.getTempDir();
+
+ const tempFileName = PathUtils.join(tmpDir, "test_stat_btime.tmp");
+ await createFile(tempFileName);
+ const originalInfo = await IOUtils.stat(tempFileName);
+
+ const future = originalInfo.lastModified + 6000;
+ await IOUtils.touch(tempFileName, future);
+ const futureInfo = await IOUtils.stat(tempFileName);
+
+ ok(originalInfo.hasOwnProperty("creationTime"), "originalInfo has creationTime field");
+ ok(originalInfo.creationTime !== undefined && originalInfo.creationTime !== null, "originalInfo has non-null creationTime");
+
+ ok(futureInfo.hasOwnProperty("creationTime"), "futureInfo has creationTime field");
+ ok(futureInfo.creationTime !== undefined && futureInfo.creationTime !== null, "futureInfo has non-null creationTime");
+
+ is(originalInfo.creationTime, futureInfo.creationTime, "creationTime matches");
+
+ await cleanup(tempFileName);
+ } else {
+ ok(true, `skipping test_stat_btime() on unsupported platform ${Services.appinfo.OS}`);
+ }
+ });
+
+ add_task(async function test_touch_failures() {
+ info("Test attempt to touch a non-existing file");
+ const tmpDir = await PathUtils.getTempDir();
+ const notExistsFile = PathUtils.join(tmpDir, "test_touch_not_exists.tmp");
+
+ await Assert.rejects(
+ IOUtils.touch(notExistsFile),
+ /Could not touch file\(.*\) because it does not exist/,
+ "IOUtils::touch throws if the target file does not exist"
+ );
+
+ info("Test attempt to set modification time to Epoch");
+ const tempFileName = PathUtils.join(tmpDir, "test_touch_epoch.tmp");
+ await createFile(tempFileName);
+
+ await Assert.rejects(
+ IOUtils.touch(tempFileName, 0),
+ /Refusing to set the modification time of file\(.*\) to 0/,
+ "IOUtils::touch cannot set the file modification time to Epoch"
+ );
+
+ await cleanup(tempFileName);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_worker.xhtml b/dom/system/tests/ioutils/test_ioutils_worker.xhtml
new file mode 100644
index 0000000000..df67d48676
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_worker.xhtml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing IOUtils 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 src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ // Test IOUtils in a chrome worker.
+ function test() {
+ // finish() will be called in the worker.
+ SimpleTest.waitForExplicitFinish();
+ info("test_ioutils_worker.xhtml: Starting test");
+
+ const worker = new ChromeWorker("file_ioutils_worker.js");
+ info("test_ioutils_worker.xhtml: Chrome worker created");
+
+ // Set up the worker with testing facilities, and start it.
+ listenForTests(worker, { verbose: false });
+ worker.postMessage(0);
+ info("test_ioutils_worker.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>