diff options
Diffstat (limited to 'dom/system/tests')
26 files changed, 3797 insertions, 0 deletions
diff --git a/dom/system/tests/chrome.toml b/dom/system/tests/chrome.toml new file mode 100644 index 0000000000..87f782b465 --- /dev/null +++ b/dom/system/tests/chrome.toml @@ -0,0 +1,6 @@ +[DEFAULT] + +["test_pathutils.html"] + +["test_pathutils_worker.xhtml"] +support-files = ["pathutils_worker.js"] diff --git a/dom/system/tests/file_bug1197901.html b/dom/system/tests/file_bug1197901.html new file mode 100644 index 0000000000..e4ca9fd380 --- /dev/null +++ b/dom/system/tests/file_bug1197901.html @@ -0,0 +1,16 @@ +<pre>Sensor events testing</pre> +<script> + +window.onmessage = function(event) { + if (event.data.command == "addEventListener") { + window.addEventListener( + "devicemotion", function() { + event.source.postMessage({ result: event.data.expected, + message: event.data.message }, + "*"); + } + ); + } +}; + +</script> diff --git a/dom/system/tests/ioutils/chrome.toml b/dom/system/tests/ioutils/chrome.toml new file mode 100644 index 0000000000..16434b5bb6 --- /dev/null +++ b/dom/system/tests/ioutils/chrome.toml @@ -0,0 +1,39 @@ +[DEFAULT] +support-files = [ + "file_ioutils_test_fixtures.js", + "file_ioutils_worker.js", +] + +["test_ioutils.html"] + +["test_ioutils_compute_hex_digest.html"] + +["test_ioutils_copy_move.html"] + +["test_ioutils_create_unique.html"] + +["test_ioutils_dir_iteration.html"] + +["test_ioutils_getfile.html"] + +["test_ioutils_mac_xattr.html"] +skip-if = ["(os != 'mac')"] + +["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_set_permissions.html"] + +["test_ioutils_stat_set_modification_time.html"] + +["test_ioutils_windows_file_attributes.html"] +skip-if = ["(os != 'win')"] + +["test_ioutils_worker.xhtml"] 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..e367eb4d99 --- /dev/null +++ b/dom/system/tests/ioutils/file_ioutils_worker.js @@ -0,0 +1,219 @@ +// Any copyright is dedicated to the Public Domain. +// - http://creativecommons.org/publicdomain/zero/1.0/ + +// Portions of this file are originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// MIT license: http://opensource.org/licenses/MIT + +/* eslint-env worker */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +importScripts("file_ioutils_test_fixtures.js"); + +self.onmessage = async function (msg) { + const tmpDir = await PathUtils.getTempDir(); + + // 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 = 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. + let fileContents = await IOUtils.read(tmpFileName); + ok( + _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( + _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 = PathUtils.join(tmpDir, "test_move_file_src.tmp"); + const dest = PathUtils.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 = PathUtils.join(tmpDir, "test_ioutils_orig.tmp"); + const destFileName = PathUtils.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 = PathUtils.join(tmpDir, "test_make_dir.tmp.d"); + await IOUtils.makeDirectory(dir); + const stat = await IOUtils.stat(dir); + is( + stat.type, + "directory", + "IOUtils::makeDirectory can make a new directory from a worker" + ); + + await cleanup(dir); + } +}; + +// This is copied from the ObjectUtils module, as it is difficult to translate +// file_ioutils_test_fixtures.js into a ES module and have it used in non-module +// contexts. + +// ... Start of previously MIT-licensed code. +// This deepEqual implementation is originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// MIT license: http://opensource.org/licenses/MIT + +function _deepEqual(a, b) { + // The numbering below refers to sections in the CommonJS spec. + + // 7.1 All identical values are equivalent, as determined by ===. + if (a === b) { + return true; + // 7.2 If the b value is a Date object, the a value is + // equivalent if it is also a Date object that refers to the same time. + } + let aIsDate = instanceOf(a, "Date"); + let bIsDate = instanceOf(b, "Date"); + if (aIsDate || bIsDate) { + if (!aIsDate || !bIsDate) { + return false; + } + if (isNaN(a.getTime()) && isNaN(b.getTime())) { + return true; + } + return a.getTime() === b.getTime(); + // 7.3 If the b value is a RegExp object, the a value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } + let aIsRegExp = instanceOf(a, "RegExp"); + let bIsRegExp = instanceOf(b, "RegExp"); + if (aIsRegExp || bIsRegExp) { + return ( + aIsRegExp && + bIsRegExp && + a.source === b.source && + a.global === b.global && + a.multiline === b.multiline && + a.lastIndex === b.lastIndex && + a.ignoreCase === b.ignoreCase + ); + // 7.4 Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + } + if (typeof a != "object" || typeof b != "object") { + return a == b; + } + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + return objEquiv(a, b); +} + +function instanceOf(object, type) { + return Object.prototype.toString.call(object) == "[object " + type + "]"; +} + +function isUndefinedOrNull(value) { + return value === null || value === undefined; +} + +function isArguments(object) { + return instanceOf(object, "Arguments"); +} + +function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { + return false; + } + // An identical 'prototype' property. + if ((a.prototype || undefined) != (b.prototype || undefined)) { + return false; + } + // Object.keys may be broken through screwy arguments passing. Converting to + // an array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = Array.prototype.slice.call(a); + b = Array.prototype.slice.call(b); + return _deepEqual(a, b); + } + let ka, kb; + try { + ka = Object.keys(a); + kb = Object.keys(b); + } catch (e) { + // Happens when one is a string literal and the other isn't + return false; + } + // Having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) { + return false; + } + // The same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + // Equivalent values for every corresponding key, and possibly expensive deep + // test + for (let key of ka) { + if (!_deepEqual(a[key], b[key])) { + return false; + } + } + return true; +} + +// ... End of previously MIT-licensed code. 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_compute_hex_digest.html b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html new file mode 100644 index 0000000000..7a98f83a1f --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html @@ -0,0 +1,55 @@ + +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_computeHexDigest() { + const tempDir = PathUtils.join(PathUtils.tempDir, "ioutils-test-compute-hex-digest.tmp.d"); + await createDir(tempDir); + + const path = PathUtils.join(tempDir, "file"); + await IOUtils.writeUTF8(path, "hello world\n"); + + const DIGESTS = [ + "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", + "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80", + "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", + ]; + const ALGORITHMS = ["sha256", "sha384", "sha512"]; + + for (let i = 0; i < ALGORITHMS.length; i++) { + const alg = ALGORITHMS[i]; + const expected = DIGESTS[i]; + + Assert.equal( + await IOUtils.computeHexDigest(path, alg), + expected, + `IOUtils.hashFile() has expected value for ${alg}`); + } + + await cleanup(tempDir); + }); + </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..408bb82f39 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html @@ -0,0 +1,360 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_move_relative_path() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_src.tmp"); + const destFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp"); + const destDir = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp.d"); + const dest = PathUtils.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(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "test_move_dir.tmp.d"); + const destDir = PathUtils.join(PathUtils.tempDir, "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(PathUtils.join(srcDir, "file.tmp"), "foo"); + // Test. + await IOUtils.move(srcDir, destDir); + const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp"); + ok( + !await IOUtils.exists(srcDir) + && await dirExists(destDir) + && await dirExists(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "not_exists_src.tmp"); + const notExistsDest = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_move_failures_file_dest.tmp"); + const srcDir = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_orig.tmp"); + const destFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp"); + const destDir = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp.d"); + const dest = PathUtils.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(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "test_copy_dir.tmp.d"); + const destDir = PathUtils.join(PathUtils.tempDir, "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(PathUtils.join(srcDir, "file.tmp"), "foo"); + // Test. + await IOUtils.copy(srcDir, destDir, { recursive: true }); + const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp"); + ok( + await dirExists(srcDir) + && await dirExists(destDir) + && await dirExists(PathUtils.join(destDir, PathUtils.filename(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 = PathUtils.join(PathUtils.tempDir, "test_copy_not_exists_src.tmp"); + const notExistsDest = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_copy_failures_file_dest.tmp"); + const srcDir = PathUtils.join(PathUtils.tempDir, "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_create_unique.html b/dom/system/tests/ioutils/test_ioutils_create_unique.html new file mode 100644 index 0000000000..be7ab23697 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_create_unique.html @@ -0,0 +1,86 @@ +<!-- 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"; + + function octalFormat(n) { + let s = n.toString(8); + while (s.length < 3) { + s = `0${s}`; + } + return `0o${s}`; + } + + async function check(method, path, prefix, type, perms) { + const filename = PathUtils.filename(path); + + ok(filename.startsWith(prefix), `IOUtils.${method} uses the prefix`); + ok(await IOUtils.exists(path), `IOUtils.${method} creates a file`); + + const stat = await IOUtils.stat(path); + is(stat.type, type, `IOUtils.${method} creates a "${type}" file`); + + is( + octalFormat(stat.permissions), + octalFormat(perms), + `IOUtils.${method} creates a file with the correct permissions` + ); + } + + add_task(async function test_createUnique() { + const tempDir = PathUtils.join( + PathUtils.tempDir, + "test_createUnique.tmp.d" + ); + + const filesToChmod = []; + + SimpleTest.registerCleanupFunction(async function test_createUnique_cleanup() { + for (const file of filesToChmod) { + if (await IOUtils.exists(file)) { + await IOUtils.setPermissions(file, 0o666); + } + } + + await IOUtils.remove(tempDir, { recursive: true }); + }); + + const isWindows = Services.appinfo.OS === "WINNT"; + + info("Creating a unique directory") + const dir = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o600); + await check("createUniqueDirectory", dir, "unique-dir", "directory", isWindows ? 0o666 : 0o600); + + info("Creating a unique directory with the same prefix") + const dir2 = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o700); + await check("createUniqueDirectory", dir2, "unique-dir", "directory", isWindows ? 0o666 : 0o700); + ok(dir !== dir2, "IOUtils.createUniqueDirectory creates unique paths"); + + info("Creating a unique file"); + const file = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o641); + await check("createUniqueFile", file, "unique-file", "regular", isWindows ? 0o666 : 0o641); + + info("Creating a unique file with the same prefix"); + const file2 = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o400); + filesToChmod.push(file2); + await check("createUniqueFile", file2, "unique-file", "regular", isWindows ? 0o444 : 0o400); + }); + </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..54168235b0 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html @@ -0,0 +1,96 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function iterate_dir_failure() { + let notExists = PathUtils.join(PathUtils.tempDir, '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 = PathUtils.join(PathUtils.tempDir, '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 = PathUtils.join(PathUtils.tempDir, 'iterator.tmp.d'); + let child1 = PathUtils.join(root, 'child1.tmp'); + let child2 = PathUtils.join(root, 'child2.tmp'); + let grandchild = PathUtils.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 = PathUtils.join(PathUtils.tempDir, '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); + }); + + add_task(async function iterate_ignore_missing_dir() { + info("Try to get the children of a missing file with ignoreAbsent"); + + const notExists = PathUtils.join(PathUtils.tempDir, "does_not_exist_dir.tmp.d"); + + is( + (await IOUtils.getChildren(notExists, { ignoreAbsent: true })).length, + 0, + "IOUtils::getChildren returns an empty array when called with ignoreAbsent on a missing file" + ); + ok(!await fileExists(notExists), `Expected ${notExists} not to exist`); + }); + </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_getfile.html b/dom/system/tests/ioutils/test_ioutils_getfile.html new file mode 100644 index 0000000000..077f0b5c1c --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_getfile.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> + "use strict"; + + const TEST_PATH = PathUtils.join(PathUtils.tempDir, "test-ioutils-getfile"); + + add_task(async function test_getFile() { + const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file.txt"); + const parentPath = PathUtils.parent(expectedPath); + + ok(!(await IOUtils.exists(parentPath)), "Parent directory should not exist"); + + const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file.txt"); + const path = file.path; + + is(path, expectedPath, "Should have the correct path"); + ok(await IOUtils.exists(parentPath), "Parent directory should be created"); + ok(!(await IOUtils.exists(path)), "File should not be created"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + + add_task(async function test_getFile_exists() { + const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt"); + + await IOUtils.makeDirectory(PathUtils.parent(expectedPath)); + await IOUtils.writeUTF8(expectedPath, "hello world"); + + const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt"); + is(file.path, expectedPath, "Should have the correct path"); + is(await IOUtils.readUTF8(file.path), "hello world", "Contents should be unchanged"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + + add_task(async function test_getDirectory() { + const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge"); + + ok(!(await IOUtils.exists(PathUtils.parent(expectedPath))), "Parent directory should not exist"); + + const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge"); + + is(file.path, expectedPath, "Should have the correct path"); + ok(await IOUtils.exists(expectedPath), "Directory should be created"); + + const info = await IOUtils.stat(expectedPath); + is(info.type, "directory", "Should create a directory"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + + add_task(async function test_getDirectory_exists() { + const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge"); + + await IOUtils.makeDirectory(expectedPath); + + const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge"); + is(file.path, expectedPath, "Should have the correct path"); + ok(await IOUtils.exists(expectedPath), "Directory should still exist"); + + const info = await IOUtils.stat(expectedPath); + is(info.type, "directory", "Should still be a directory"); + + await IOUtils.remove(TEST_PATH, { recursive: true }); + }); + </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_mac_xattr.html b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html new file mode 100644 index 0000000000..6af9b2e6f8 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html @@ -0,0 +1,91 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + const ATTR = "bogus.attr"; + const VALUE = new TextEncoder().encode("bogus"); + + add_task(async function test_macXAttr() { + const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-macos-xattr.tmp.d"); + + await createDir(tmpDir); + + const path = PathUtils.join(tmpDir, "file.tmp"); + ok(!await IOUtils.exists(path), "File should not exist"); + await IOUtils.writeUTF8(path, ""); + + ok( + !await IOUtils.hasMacXAttr(path, ATTR), + "File does not have an extended attribute at creation" + ); + + info("Testing getting an attribute that does not exist"); + await Assert.rejects( + IOUtils.getMacXAttr(path, ATTR), + /NotFoundError: The file `.+' does not have an extended attribute/, + "IOUtils::getMacXAttr rejects when the attribute does not exist" + ); + + info("Testing setting an attribute"); + await IOUtils.setMacXAttr(path, ATTR, VALUE); + ok( + await IOUtils.hasMacXAttr(path, ATTR), + "File has extended attribute after setting" + ); + + { + info("Testing getting an attribute") + const value = await IOUtils.getMacXAttr(path, ATTR); + Assert.deepEqual( + Array.from(value), + Array.from(VALUE), + "Attribute value should match" + ); + } + + info("Testing removing an attribute"); + await IOUtils.delMacXAttr(path, ATTR); + await Assert.rejects( + IOUtils.getMacXAttr(path, ATTR), + /NotFoundError: The file `.+' does not have an extended attribute/, + "IOUtils::delMacXAttr removes the attribute" + ); + + ok( + !await IOUtils.hasMacXAttr(path, ATTR), + "File does not have extended attribute after removing" + ); + + info("Testing removing an attribute that does not exist"); + await Assert.rejects( + IOUtils.delMacXAttr(path, ATTR), + /NotFoundError: The file `.+' does not have an extended attribute/, + "IOUtils::delMacXAttr rejects when the attribute does not exist" + ); + + await cleanup(tmpDir); + }); + </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..6827b24cc6 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html @@ -0,0 +1,135 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_make_directory() { + info("Test creating a new directory"); + const newDirectoryName = PathUtils.join(PathUtils.tempDir, "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(PathUtils.tempDir, "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 .*/, + "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 notADirFileName = PathUtils.join(PathUtils.tempDir, "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 newDir = PathUtils.join(PathUtils.tempDir, "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); + }); + + add_task(async function test_make_directory_root() { + if (Services.appinfo.OS === "WINNT") { + // We don't actually know the root drive, but we can find the root drive + // of the profile directory. + let current = PathUtils.profileDir; + let parent = PathUtils.parent(current); + while (parent !== null) { + current = parent; + parent = PathUtils.parent(current); + } + // `current` will now be a valid root directory. + ok(await IOUtils.exists(current), "Root directory should exist"); + + const DRIVE_RE = /^[A-Za-z]:$/; + ok( + current.startsWith("\\\\") || DRIVE_RE.test(current), + `Root directory (${current}) should be a UNC path or drive`, + ); + await IOUtils.makeDirectory(current, {createAncestors: false}); + } else { + ok(await IOUtils.exists("/"), "Root directory should exist"); + await IOUtils.makeDirectory("/", {createAncestors: false}); + } + }); + </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..2243eb1eda --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write.html @@ -0,0 +1,524 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + const { ObjectUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ObjectUtils.sys.mjs" + ); + + add_task(async function test_read_failure() { + const doesNotExist = PathUtils.join(PathUtils.tempDir, "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() { + // Make a new file, and try to write to it with overwrites disabled. + const tmpFileName = PathUtils.join(PathUtils.tempDir, "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, { + mode: "create", + }), + /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, + { mode: "overwrite" } + ); + 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"); + + let fileContents = new TextEncoder().encode("Original file contents"); + let destFileName = PathUtils.join(PathUtils.tempDir, "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"); + + let fileContents = new TextEncoder().encode("Original file contents"); + let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_and_tmp_options.tmp"); + let backupFileName = destFileName + ".backup"; + let tmpFileName = PathUtils.join(PathUtils.tempDir, "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"); + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + 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" + ); + + info("Test backup with tmp and backup file options, existing destination and backup"); + newFileContents = new TextEncoder().encode("Updated new file contents"); + ok(await fileExists(destFileName), `Expected ${destFileName} to exist`); + ok(await fileExists(backupFileName), `Expected ${backupFileName} to exist`); + bytesWritten = + await IOUtils.write(destFileName, newFileContents, { + backupFile: backupFileName, + tmpPath: tmpFileName, + }); + + ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile"); + ok( + await fileHasTextContents(backupFileName, "New file contents"), + "IOUtils::write can create a backup if the target file exists" + ); + ok( + await fileHasTextContents(destFileName, "Updated 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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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. + + info("Test writing to a new binary file"); + const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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_bad_call() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tmpFileName = PathUtils.join(PathUtils.tempDir, "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); + }); + + add_task(async function test_write_directory() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_directory.tmp"); + const tmpPath = `${fileName}.tmp`; + const bytes = Uint8Array.of(1, 2, 3, 4); + + await IOUtils.makeDirectory(fileName); + await Assert.rejects( + IOUtils.write(fileName, bytes), + /NotAllowedError: Could not open the file at .+ for writing/); + + await Assert.rejects( + IOUtils.write(fileName, bytes, { tmpPath }), + /NotAllowedError: Could not open the file at .+ for writing/); + + ok(!await IOUtils.exists(PathUtils.join(fileName, PathUtils.filename(tmpPath)))); + }); + + add_task(async function test_read_offset() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_offset.tmp"); + + const bytes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + const byteArray = Uint8Array.of(...bytes); + + await IOUtils.write(tmpFileName, byteArray); + + for (const offset of [0, 5]) { + info(`Reading bytes from offset ${offset}`); + + const readBytes = await IOUtils.read(tmpFileName, { offset }); + Assert.deepEqual( + Array.from(readBytes), + bytes.slice(offset), + `should have read bytes from offset ${offset}` + ); + } + + for (const offset of [0, 5]) { + info(`Reading up to 5 bytes from offset ${offset}`); + + const readBytes = await IOUtils.read(tmpFileName, {offset, maxBytes: 5}); + Assert.deepEqual( + Array.from(readBytes), + bytes.slice(offset, offset + 5), + `should have read 5 bytes from offset ${offset}` + ); + } + + { + info(`Reading bytes from offset 10`); + const readBytes = await IOUtils.read(tmpFileName, {offset: 10}); + is(readBytes.length, 0, "should have read 0 bytes"); + } + + { + info(`Reading up to 10 bytes from offset 5`); + const readBytes = await IOUtils.read(tmpFileName, {offset: 5, maxBytes: 10}); + is(readBytes.length, 5, "should have read 5 bytes"); + Assert.deepEqual( + Array.from(readBytes), + bytes.slice(5, 10), + "should have read last 5 bytes" + ); + } + }); + + add_task(async function test_write_appendOrCreate() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_appendOrCreate.tmp"); + + await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4), { mode: "appendOrCreate" }); + + { + const contents = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4], "read bytes should be equal"); + } + + await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "appendOrCreate" }); + + { + const contents = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appendOrCreateing"); + } + + await cleanup(fileName); + }); + + add_task(async function test_write_append() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append.tmp"); + + await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4)); + + const beforeAppend = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(beforeAppend), [0, 1, 2, 3, 4], "read bytes should be equal"); + + await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" }); + + const afterAppend = await IOUtils.read(fileName); + Assert.deepEqual(Array.from(afterAppend), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appending"); + + await cleanup(fileName); + }); + + add_task(async function test_write_append_no_create() { + const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append_no_create.tmp"); + + await Assert.rejects( + IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" }), + /NotFoundError: Could not open the file at .*/ + ); + }); + </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..0acb191e1b --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html @@ -0,0 +1,193 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + const { ObjectUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ObjectUtils.sys.mjs" + ); + + 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 filename = PathUtils.join(PathUtils.tempDir, "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(PathUtils.tempDir, "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(PathUtils.tempDir, "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 filename = PathUtils.join(PathUtils.tempDir, "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); + }); + + add_task(async function test_append_json() { + const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp"); + + await IOUtils.writeJSON(filename, OBJECT); + + await Assert.rejects( + IOUtils.writeJSON(filename, OBJECT, {mode: "append"}), + /NotSupportedError: IOUtils.writeJSON does not support appending to files/, + "IOUtils.writeJSON() cannot append" + ); + + await cleanup(filename); + }); + + add_task(async function test_read_json_bom() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_bom.tmp"); + const raw = `\uFEFF${JSON.stringify({hello: "world"})}`; + await IOUtils.writeUTF8(tmpFileName, raw); + + ok( + ObjectUtils.deepEqual( + await IOUtils.readJSON(tmpFileName), + { hello: "world" }, + ), + "IOUtils.readJSON should skip BOM" + ); + + await IOUtils.writeUTF8(tmpFileName, raw, { compress: true }); + + ok( + ObjectUtils.deepEqual( + await IOUtils.readJSON(tmpFileName, { decompress: true }), + { hello: "world" }, + ), + "IOUtils.readJSON should skip BOM for compressed 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_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html new file mode 100644 index 0000000000..cdea016732 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html @@ -0,0 +1,384 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + // 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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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, { + mode: "create", + }), + /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, + { mode: "overwrite" } + ); + 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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_and_tmp_options.tmp"); + let backupFileName = destFileName + ".backup"; + let tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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_bad_call() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "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); + }); + + add_task(async function test_skipBOM() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_readutf8_bom.tmp"); + + const raw = `\uFEFFstring`; + + await IOUtils.writeUTF8(tmpFileName, raw); + + is( + await IOUtils.readUTF8(tmpFileName), + "string", + "IOUtils.readUTF8 should skip BOM by default" + ); + + await IOUtils.writeUTF8(tmpFileName, raw, { compress: true }); + + is( + await IOUtils.readUTF8(tmpFileName, { decompress: true }), + "string", + "IOUtils.readUTF8 should skip BOM by default for compressed 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..f368fc09d3 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_remove.html @@ -0,0 +1,118 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_create_and_remove_file() { + info("Test creating and removing a single file"); + const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 tempDirName = PathUtils.join(PathUtils.tempDir, "test_ioutils_create_and_remove.tmp.d"); + await IOUtils.makeDirectory(tempDirName); + ok(await dirExists(tempDirName), `Expected directory ${tempDirName} to exist`); + + await IOUtils.remove(tempDirName); + ok(!await dirExists(tempDirName), "IOUtils::remove can remove empty directories"); + }); + + add_task(async function test_remove_non_existing() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove.tmp.d"); + const tmpChildDir = PathUtils.join(tmpParentDir, "child.tmp.d"); + const tmpTopLevelFileName = PathUtils.join(tmpParentDir, "top.tmp"); + const tmpNestedFileName = PathUtils.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" + ); + }); + + if (Services.appinfo.OS === "WINNT") { + add_task(async function test_remove_retry_readonly() { + + const tmpDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove_retry_readonly.tmp.d"); + const path = PathUtils.join(tmpDir, "file.txt"); + + await createDir(tmpDir); + await createFile(path, ""); + + await IOUtils.setWindowsAttributes(path, { readOnly: true }); + + await Assert.rejects( + IOUtils.remove(path), + /NotAllowedError/, + "Cannot remove a readonly file by default" + ); + + Assert.ok(await fileExists(path), "File should still exist"); + + await IOUtils.remove(path, { retryReadonly: true }); + + Assert.ok(!await fileExists(path), "File should not exist"); + + await IOUtils.remove(tmpDir); + }); + } + </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..36f7dab72a --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_set_permissions.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"; + + add_task(async function test_setPermissions() { + const tempFile = PathUtils.join(PathUtils.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 { + let umask = Services.sysinfo.getProperty("umask"); + is(stat.permissions, 0o421 & ~umask, "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); + }); + + add_task(async function test_setPermissionsWithoutHonoringUmask() { + const tempFile = PathUtils.join(PathUtils.tempDir, "setPermissions.tmp"); + + await IOUtils.writeUTF8(tempFile, ""); + await IOUtils.setPermissions(tempFile, 0o421, false); + + 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_set_modification_time.html b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html new file mode 100644 index 0000000000..e508817a41 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html @@ -0,0 +1,242 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function test_stat() { + info("Test attempt to stat a regular empty file"); + + const emptyFileName = PathUtils.join(PathUtils.tempDir, "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(PathUtils.tempDir, "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(PathUtils.tempDir, "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" + ); + Assert.less( + (fileInfo.lastAccessed - new Date().valueOf()), + 1000, + "IOUtils::stat can get the last access 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 notExistsFile = PathUtils.join(PathUtils.tempDir, "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_setModificationTime_and_stat() { + const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp"); + { + info("Test attempt to setModificationTime a file"); + await createFile(tmpFileName); + + const oldFileInfo = await IOUtils.stat(tmpFileName); + await sleep(500); + + // Now update the time stamp. + const stamp = await IOUtils.setModificationTime(tmpFileName); + const newFileInfo = await IOUtils.stat(tmpFileName); + + ok( + newFileInfo.lastModified > oldFileInfo.lastModified, + "IOUtils::setModificationTime can update the lastModified time stamp on the file system" + ); + is( + stamp, + newFileInfo.lastModified, + "IOUtils::setModificationTime returns the updated time stamp." + ); + is( + newFileInfo.lastAccessed, + oldFileInfo.lastAccessed, + "IOUtils::setModificationTime does not change lastAccessed" + ); + + await sleep(500); + + const newerStamp = await IOUtils.setAccessTime(tmpFileName); + const newerFileInfo = await IOUtils.stat(tmpFileName); + + ok( + newerFileInfo.lastAccessed > newFileInfo.lastAccessed, + "IOUtils::setAccessTime can update the lastAccessed time stamp on the file system" + ); + is( + newerStamp, + newerFileInfo.lastAccessed, + "IOUtils::setAccessTime returns the updated time stamp." + ); + is( + newerFileInfo.lastModified, + newFileInfo.lastModified, + "IOUtils::setAccessTime does not change lastModified" + ); + } + + const tmpDirName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp.d"); + { + info("Test attempt to setModificationTime a directory"); + await createDir(tmpDirName); + + const oldFileInfo = await IOUtils.stat(tmpDirName); + await sleep(500); + + const stamp = await IOUtils.setModificationTime(tmpDirName); + const newFileInfo = await IOUtils.stat(tmpDirName); + + ok( + newFileInfo.lastModified > oldFileInfo.lastModified, + "IOUtils::setModificationTime can update the lastModified time stamp on a directory" + ); + is( + stamp, + newFileInfo.lastModified, + "IOUtils::setModificationTime returns the updated time stamp on a directory" + ); + } + + await cleanup(tmpFileName, tmpDirName); + }); + + add_task(async function test_setModificationTime_custom_mod_time() { + const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_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.setModificationTime(tempFileName, future); + const futureInfo = await IOUtils.stat(tempFileName); + Assert.less(originalInfo.lastModified, futureInfo.lastModified, "IOUtils::setModificationTime can set a future modification time for the file"); + + is(newModTime, futureInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp"); + is(newModTime, future, "IOUtils::setModificationTime 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.setModificationTime(tempFileName, past); + const pastInfo = await IOUtils.stat(tempFileName); + Assert.greater(originalInfo.lastModified, pastInfo.lastModified, "IOUtils::setModificationTime can set a past modification time for the file"); + + is(newModTime, pastInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp"); + is(newModTime, past, "IOUtils::setModificationTime 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 tempFileName = PathUtils.join(PathUtils.tempDir, "test_stat_btime.tmp"); + await createFile(tempFileName); + const originalInfo = await IOUtils.stat(tempFileName); + + const future = originalInfo.lastModified + 6000; + await IOUtils.setModificationTime(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_setModificationTime_failures() { + info("Test attempt to setModificationTime a non-existing file"); + const notExistsFile = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_not_exists.tmp"); + + await Assert.rejects( + IOUtils.setModificationTime(notExistsFile), + /Could not set modification time of file\(.*\) because it does not exist/, + "IOUtils::setModificationTime throws if the target file does not exist" + ); + + info("Test attempt to set modification time to Epoch"); + const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_epoch.tmp"); + await createFile(tempFileName); + + await Assert.rejects( + IOUtils.setModificationTime(tempFileName, 0), + /Refusing to set the modification time of file\(.*\) to 0/, + "IOUtils::setModificationTime 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_windows_file_attributes.html b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html new file mode 100644 index 0000000000..a5b72bd078 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html @@ -0,0 +1,137 @@ +<!-- 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.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + add_task(async function getSetWindowsAttributes() { + const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-windows-attributes.tmp.d"); + await createDir(tmpDir); + ok(await dirExists(tmpDir), `Expected ${tmpDir} to be a directory`); + + const filePath = PathUtils.join(tmpDir, "file.tmp"); + await createFile(filePath); + ok(await fileExists(filePath), `Expected ${filePath} to exist`); + + { + info("Getting attributes for newly created file."); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Newly created file ${filePath} is not a read-only file`); + ok(attrs.hidden === false, `Newly created file ${filePath} is not a hidden file`); + ok(attrs.system === false, `Newly created file ${filePath} is not a system file`); + } + + { + info("Setting read-only on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: true }) + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is not a system file`); + } + + info("Attempting to write to a read-only file."); + + await Assert.rejects( + IOUtils.writeUTF8(filePath, "hello, world"), + /NotAllowedError: Could not open the file at .+ for writing/, + "IOUtils::writeUTF8 on a read-only file fails." + ); + + { + info("Setting hidden on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { hidden: true }) + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is not a system file`); + } + + { + info("Setting system on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { system: true }) + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is a system file`); + } + + { + info("Clearing all Windows attributes on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: false, hidden: false, system: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is not a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is not a system file`); + } + + { + info("Setting all Windows attributes on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: true, hidden: true, system: true }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is a system file`); + } + + { + info("Clearing read-only on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { readOnly: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is no longer a read-only file`); + ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is still a system file`); + } + + { + info("Clearing hidden on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { hidden: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is no longer a hidden file`); + ok(attrs.system === true, `Updated file ${filePath} is still a system file`); + } + + { + info("Clearing system on an existing file."); + await IOUtils.setWindowsAttributes(filePath, { system: false }); + const attrs = await IOUtils.getWindowsAttributes(filePath); + + ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`); + ok(attrs.hidden === false, `Updated file ${filePath} is sitll not a hidden file`); + ok(attrs.system === false, `Updated file ${filePath} is no longer a system file`); + } + + await cleanup(tmpDir); + }); + + </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> diff --git a/dom/system/tests/mochitest.toml b/dom/system/tests/mochitest.toml new file mode 100644 index 0000000000..b607853077 --- /dev/null +++ b/dom/system/tests/mochitest.toml @@ -0,0 +1,11 @@ +[DEFAULT] +tags = "condprof" +scheme = "https" + +support-files = ["file_bug1197901.html"] + +["test_bug1197901.html"] +skip-if = [ + "os == 'android'", + "condprof", #: "Only focused window should get the sensor events" +] diff --git a/dom/system/tests/pathutils_worker.js b/dom/system/tests/pathutils_worker.js new file mode 100644 index 0000000000..9e4742b764 --- /dev/null +++ b/dom/system/tests/pathutils_worker.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env worker */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +self.onmessage = async function (message) { + let expected = message.data; + info("ON message"); + info(JSON.stringify(expected)); + const profileDir = await PathUtils.getProfileDir(); + is( + profileDir, + expected.profileDir, + "PathUtils.profileDir() in a worker should match PathUtils.profileDir on main thread" + ); + + const localProfileDir = await PathUtils.getLocalProfileDir(); + is( + localProfileDir, + expected.localProfileDir, + "PathUtils.getLocalProfileDir() in a worker should match PathUtils.localProfileDir on main thread" + ); + + const tempDir = await PathUtils.getTempDir(); + is( + tempDir, + expected.tempDir, + "PathUtils.getTempDir() in a worker should match PathUtils.tempDir on main thread" + ); + + finish(); +}; diff --git a/dom/system/tests/test_bug1197901.html b/dom/system/tests/test_bug1197901.html new file mode 100644 index 0000000000..7e1866ffa3 --- /dev/null +++ b/dom/system/tests/test_bug1197901.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1197901 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1197901</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1197901 **/ + SimpleTest.requestFlakyTimeout("requestFlakyTimeout is silly"); + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [["device.sensors.test.events", true]]}, + doTest); + }, window); + }; + + function doTest() { + window.onmessage = function(event) { + ok(event.data.result, event.data.message); + }; + + // Only same-origin iframe should get the events. + var xo = document.getElementById("cross-origin"); + xo.contentWindow.postMessage( + { command: "addEventListener", + expected: false, + message: "Cross-origin iframe shouldn't get the sensor events."}, + "*"); + + var so = document.getElementById("same-origin"); + so.contentWindow.postMessage( + { command: "addEventListener", + expected: true, + message: "Same-origin iframe should get the sensor events." }, + "*"); + + // We need a timeout here to check that something does not happen. + setTimeout(function() { + so.remove(); + xo.remove(); + doWindowTest(); + }, 500); + } + + function doWindowTest() { + var win = window.open("file_bug1197901.html", "w1", "height=100,width=100"); + win.onload = function() { + win.focus(); + SimpleTest.waitForFocus(function() { + var win2 = window.open("file_bug1197901.html", "w2", "height=100,width=100,left=100"); + win2.onload = function() { + win2.focus(); + SimpleTest.waitForFocus(function() { + // Only focused window should get the events. + win.postMessage( + { command: "addEventListener", + expected: false, + message: "Only focused window should get the sensor events." }, + "*"); + win2.postMessage( + { command: "addEventListener", + expected: true, + message: "Focused window should get the sensor events." }, + "*"); + setTimeout(function() { + window.onmessage = null; + win.close(); + win2.close(); + SimpleTest.finish(); + }, 500); + }, win2); + }; + }, win); + }; + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<iframe src="file_bug1197901.html" id="same-origin"></iframe> +<iframe src="http://example.com/tests/dom/system/tests/file_bug1197901.html" id="cross-origin"></iframe> +</body> +</html> diff --git a/dom/system/tests/test_pathutils.html b/dom/system/tests/test_pathutils.html new file mode 100644 index 0000000000..4391de77fd --- /dev/null +++ b/dom/system/tests/test_pathutils.html @@ -0,0 +1,602 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + +<head> + <meta charset="utf-8"> + <title>PathUtils tests</title> +</head> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script> +<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<script> + "use strict"; + + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + const UNRECOGNIZED_PATH = /Could not initialize path: NS_ERROR_FILE_UNRECOGNIZED_PATH/; + const SPLIT_RELATIVE_ABSOLUTE = /PathUtils.splitRelative requires a relative path/; + const SPLIT_RELATIVE_EMPTY = /PathUtils.splitRelative: Empty directory components \(""\) not allowed by options/; + const SPLIT_RELATIVE_PARENT = /PathUtils.splitRelative: Parent directory components \("\.\."\) not allowed by options/; + const SPLIT_RELATIVE_CURRENT = /PathUtils.splitRelative: Current directory components \("\."\) not allowed by options/; + const EMPTY_PATH = /PathUtils does not support empty paths/; + const JOIN = /Could not append to path/; + + add_task(function test_filename() { + Assert.throws( + () => PathUtils.filename(""), + EMPTY_PATH, + "PathUtils.filename() does not support empty paths" + ); + Assert.throws( + () => PathUtils.filename("foo.txt"), + UNRECOGNIZED_PATH, + "PathUtils.filename() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.filename("C:"), + "C:", + "PathUtils.filename() with a drive path" + ); + is( + PathUtils.filename("C:\\"), + "C:", + "PathUtils.filename() with a drive path" + ); + is( + PathUtils.filename("C:\\Windows"), + "Windows", + "PathUtils.filename() with a path with 2 components" + ); + is( + PathUtils.filename("C:\\Windows\\"), + "Windows", + "PathUtils.filename() with a path with 2 components and a trailing slash" + ); + is( + PathUtils.filename("C:\\Windows\\System32"), + "System32", + "PathUtils.filename() with a path with 3 components" + ); + is( + PathUtils.filename("\\\\server"), + "\\\\server", + "PathUtils.filename() with a UNC server path" + ); + is( + PathUtils.filename("C:\\file.dat"), + "file.dat", + "PathUtils.filename() with a file path" + ); + } else { + is( + PathUtils.filename("/"), + "/", + "PathUtils.filename() with a root path" + ); + is( + PathUtils.filename("/usr/"), + "usr", + "PathUtils.filename() with a non-root path" + ); + is( + PathUtils.filename("/usr/lib/libfoo.so"), + "libfoo.so", + "PathUtils.filename() with a path with 3 components" + ); + } + }); + + add_task(function test_parent() { + Assert.throws( + () => PathUtils.parent("."), + UNRECOGNIZED_PATH, + "PathUtils.parent() does not support relative paths" + ); + Assert.throws( + () => PathUtils.parent(""), + EMPTY_PATH, + "PathUtils.parent() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.parent("C:"), + null, + "PathUtils.parent() with a drive path" + ); + is( + PathUtils.parent("\\\\server"), + null, + "PathUtils.parent() with a UNC server path" + ); + is( + PathUtils.parent("\\\\server\\foo"), + "\\\\server", + "PathUtils.parent() with a UNC server path and child component" + ); + + Assert.throws( + () => PathUtils.parent("C:", -1), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a negative depth throws" + ); + Assert.throws( + () => PathUtils.parent("C:", 0), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a zero depth throws" + ); + + { + const path = "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles\\foo.default"; + + const expected = [ + "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles", + "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox", + "C:\\Users\\User\\AppData\\Local\\Mozilla", + "C:\\Users\\User\\AppData\\Local", + "C:\\Users\\User\\AppData", + "C:\\Users\\User", + "C:\\Users", + "C:", + null, + ]; + + for (const [i, parent] of expected.entries()) { + is(PathUtils.parent(path, i + 1), parent, `PathUtils.parent() with depth=${i + 1}`) + } + } + } else { + is( + PathUtils.parent("/"), + null, + "PathUtils.parent() with a root path" + ); + is( + PathUtils.parent("/var"), + "/", + "PathUtils.parent() with a 2 component path" + ); + is( + PathUtils.parent("/var/run"), + "/var", + "PathUtils.parent() with a 3 component path" + ); + + Assert.throws( + () => PathUtils.parent("/", -1), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a negative depth throws" + ); + Assert.throws( + () => PathUtils.parent("/", 0), + /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/, + "PathUtils.parent() with a zero depth throws" + ); + + { + const path = "/home/user/.mozilla/firefox/foo.default"; + const expected = [ + "/home/user/.mozilla/firefox", + "/home/user/.mozilla", + "/home/user", + "/home", + "/", + null, + ]; + + for (const [i, parent] of expected.entries()) { + is( + PathUtils.parent(path, i + 1), + parent, + `PathUtils.parent() with depth=${i + 1}` + ); + } + } + } + }); + + add_task(function test_join() { + Assert.throws( + () => PathUtils.join(), + EMPTY_PATH, + "PathUtils.join() does not support empty paths" + ); + Assert.throws( + () => PathUtils.join(""), + EMPTY_PATH, + "PathUtils.join() does not support empty paths" + ); + Assert.throws( + () => PathUtils.join("foo", "bar"), + UNRECOGNIZED_PATH, + "PathUtils.join() does not support relative paths" + ); + Assert.throws( + () => PathUtils.join("."), + UNRECOGNIZED_PATH, + "PathUtils.join() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.join("C:"), + "C:", + "PathUtils.join() with a single path" + ); + is( + PathUtils.join("C:\\Windows", "System32"), + "C:\\Windows\\System32", + "PathUtils.join() with a 2 component path and an additional component" + ); + is( + PathUtils.join("C:", "Users", "Example"), + "C:\\Users\\Example", + "PathUtils.join() with a root path and two additional components" + ); + is( + PathUtils.join("\\\\server", "Files", "Example.dat"), + "\\\\server\\Files\\Example.dat", + "PathUtils.join() with a server path" + ); + } else { + is( + PathUtils.join("/"), + "/", + "PathUtils.join() with a root path" + ); + is( + PathUtils.join("/usr", "lib"), + "/usr/lib", + "PathUtils.join() with a 2 component path and an additional component" + ); + is( + PathUtils.join("/", "home", "example"), + "/home/example", + "PathUtils.join() with a root path and two additional components" + ); + } + }); + + add_task(function test_join_relative() { + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.joinRelative("C:", ""), + "C:", + "PathUtils.joinRelative() with an empty relative path" + ); + + is( + PathUtils.joinRelative("C:", "foo\\bar\\baz"), + "C:\\foo\\bar\\baz", + "PathUtils.joinRelative() with a relative path containing path separators" + ); + } else { + is( + PathUtils.joinRelative("/", ""), + "/", + "PathUtils.joinRelative() with an empty relative path" + ); + + is( + PathUtils.joinRelative("/", "foo/bar/baz"), + "/foo/bar/baz", + "PathUtils.joinRelative() with a relative path containing path separators" + ); + } + }); + + add_task(async function test_normalize() { + Assert.throws( + () => PathUtils.normalize(""), + EMPTY_PATH, + "PathUtils.normalize() does not support empty paths" + ); + Assert.throws( + () => PathUtils.normalize("."), + UNRECOGNIZED_PATH, + "PathUtils.normalize() does not support relative paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.normalize("C:\\\\Windows\\\\..\\\\\\.\\Users\\..\\Windows"), + "C:\\Windows", + "PathUtils.normalize() with a non-normalized path" + ); + } else { + // nsLocalFileUnix::Normalize() calls realpath, which resolves symlinks + // and requires the file to exist. + // + // On Darwin, the temp directory is located in `/private/var`, which is a + // symlink to `/var`, so we need to pre-normalize our temporary directory + // or expected paths won't match. + const tmpDir = PathUtils.join( + PathUtils.normalize(PathUtils.tempDir), + "pathutils_test" + ); + + await IOUtils.makeDirectory(tmpDir, { ignoreExisting: true }); + info(`created tmpDir ${tmpDir}`); + SimpleTest.registerCleanupFunction(async () => { + await IOUtils.remove(tmpDir, { + recursive: true, + }); + }); + + await IOUtils.makeDirectory(PathUtils.join(tmpDir, "foo", "bar"), { + createAncestors: true, + }); + + is( + PathUtils.normalize("/"), + "/", + "PathUtils.normalize() with a normalized path" + ); + + is( + PathUtils.normalize( + PathUtils.join( + tmpDir, + "foo", + ".", + "bar", + ".", + ) + ), + PathUtils.join(tmpDir, "foo", "bar"), + "PathUtils.normalize() with a non-normalized path" + ); + } + }); + + add_task(function test_split() { + Assert.throws( + () => PathUtils.split("foo"), + UNRECOGNIZED_PATH, + "PathUtils.split() does not support relative paths" + ); + Assert.throws( + () => PathUtils.split(""), + EMPTY_PATH, + "PathUtils.split() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + Assert.deepEqual( + PathUtils.split("C:\\Users\\Example"), + ["C:", "Users", "Example"], + "PathUtils.split() on an absolute path" + ); + + Assert.deepEqual( + PathUtils.split("C:\\Users\\Example\\"), + ["C:", "Users", "Example"], + "PathUtils.split() on an absolute path with a trailing slash" + ); + + Assert.deepEqual( + PathUtils.split("\\\\server\\Files\\Example.dat"), + ["\\\\server", "Files", "Example.dat"], + "PathUtils.split() with a server as the root" + ); + } else { + Assert.deepEqual( + PathUtils.split("/home/foo"), + ["/", "home", "foo"], + "PathUtils.split() on absolute path" + ); + + Assert.deepEqual( + PathUtils.split("/home/foo/"), + ["/", "home", "foo"], + "PathUtils.split() on absolute path with trailing slash" + ); + } + }); + + add_task(function test_splitRelative() { + Assert.throws( + () => PathUtils.splitRelative(""), + EMPTY_PATH, + "PathUtils.splitRelative() should throw with empty path" + ); + + if (Services.appinfo.OS === "WINNT") { + Assert.throws( + () => PathUtils.splitRelative("C:\\"), + SPLIT_RELATIVE_ABSOLUTE, + "PathUtils.splitRelative() should throw with a drive path" + ); + + Assert.throws( + () => PathUtils.splitRelative("\\\\server\\share\\"), + SPLIT_RELATIVE_ABSOLUTE, + "PathUtils.splitRelative() should throw with a UNC path" + ); + + Assert.throws( + () => PathUtils.splitRelative("foo\\\\bar"), + SPLIT_RELATIVE_EMPTY, + "PathUtils.splitRelative() should throw with empty component by default" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo\\\\bar", { allowEmpty: true }), + ["foo", "", "bar"], + "PathUtils.splitRelative() with an empty component is allowed with option" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo\\"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component and a trailing slash" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo\\bar"), + ["foo", "bar"], + "PathUtils.splitRelative() on a relative path with two components" + ); + } else { + Assert.throws( + () => PathUtils.splitRelative("/foo/bar"), + SPLIT_RELATIVE_ABSOLUTE, + "PathUtils.splitRelative() should throw with an absolute path" + ); + + Assert.throws( + () => PathUtils.splitRelative("foo//bar"), + SPLIT_RELATIVE_EMPTY, + "PathUtils.splitRelative() should throw with empty component by default" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo//bar", { allowEmpty: true }), + ["foo", "", "bar"], + "PathUtils.splitRelative() with an empty component is allowed with option" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo/"), + ["foo"], + "PathUtils.splitRelative() on a relative path with one component and a trailing slash" + ); + + Assert.deepEqual( + PathUtils.splitRelative("foo/bar"), + ["foo", "bar"], + "PathUtils.splitRelative() on a relative path with two components", + ); + } + + Assert.throws( + () => PathUtils.splitRelative("."), + SPLIT_RELATIVE_CURRENT, + "PathUtils.splitRelative() with a current dir component is disallowed by default" + ); + + Assert.throws( + () => PathUtils.splitRelative(".."), + SPLIT_RELATIVE_PARENT, + "PathUtils.splitRelative() with a parent dir component is disallowed by default" + ); + + Assert.deepEqual( + PathUtils.splitRelative(".", { allowCurrentDir: true }), + ["."], + "PathUtils.splitRelative() with a current dir component is allowed with option" + ); + + Assert.deepEqual( + PathUtils.splitRelative("..", { allowParentDir: true }), + [".."], + "PathUtils.splitRelative() with a parent dir component is allowed with option" + ); + }); + + add_task(function test_toFileURI() { + Assert.throws( + () => PathUtils.toFileURI("."), + UNRECOGNIZED_PATH, + "PathUtils.toFileURI() does not support relative paths" + ); + Assert.throws( + () => PathUtils.toFileURI(""), + EMPTY_PATH, + "PathUtils.toFileURI() does not support empty paths" + ); + + if (Services.appinfo.OS === "WINNT") { + is( + PathUtils.toFileURI("C:\\"), + "file:///C:", + "PathUtils.toFileURI() with a root path" + ); + + is( + PathUtils.toFileURI("C:\\Windows\\"), + "file:///C:/Windows", + "PathUtils.toFileURI() with a non-root directory path" + ); + + is( + PathUtils.toFileURI("C:\\Windows\\system32\\notepad.exe"), + "file:///C:/Windows/system32/notepad.exe", + "PathUtils.toFileURI() with a file path" + ); + } else { + is( + PathUtils.toFileURI("/"), + "file:///", + "PathUtils.toFileURI() with a root path" + ); + + is( + PathUtils.toFileURI("/bin"), + "file:///bin", + "PathUtils.toFileURI() with a non-root directory path" + ); + + is( + PathUtils.toFileURI("/bin/ls"), + "file:///bin/ls", + "PathUtils.toFileURI() with a file path" + ); + } + }); + + add_task(async function test_isAbsolute() { + if (Services.appinfo.OS === "WINNT") { + ok(PathUtils.isAbsolute("C:"), "Drive paths are absolute paths on Windows"); + ok(PathUtils.isAbsolute("C:\\Windows"), "Paths from the root are absolute paths on Windows"); + ok(!PathUtils.isAbsolute("foo"), "Paths containing a single item are not absolute paths on Windows"); + ok(!PathUtils.isAbsolute(".\\foo"), "Paths relative to the current working directory are not absolute paths on Windows"); + ok(!PathUtils.isAbsolute("..\\foo"), "Paths relative to the parent directory are not absolute paths on Windows"); + } else { + ok(PathUtils.isAbsolute("/"), "Root paths are absolute paths"); + ok(PathUtils.isAbsolute("/home"), "Paths with a root stem are absolute paths"); + ok(!PathUtils.isAbsolute("foo"), "Paths containing a single non-root item are not absolute paths"); + ok(!PathUtils.isAbsolute("./foo"), "Paths relative to the current working directory are not absolute paths"); + ok(!PathUtils.isAbsolute("../foo"), "Paths relative to the parent directory are not absolute paths"); + } + }); + + add_task(async function test_getDirectories() { + // See: nsAppDirectoryServiceDefs.h + const tests = [ + ["profileDir", "ProfD"], + ["localProfileDir", "ProfLD"], + ["tempDir", "TmpD"], + ]; + + for (const [attrName, dirConstant] of tests) { + const expected = Services.dirsvc.get(dirConstant, Ci.nsIFile).path; + + const attrValue = PathUtils[attrName]; + is(attrValue, expected, `PathUtils.${attrName} == Services.dirsvc.get("${dirConstant}", Ci.nsIFile).path`); + } + }); +</script> + +<body> +</body> + +</html> diff --git a/dom/system/tests/test_pathutils_worker.xhtml b/dom/system/tests/test_pathutils_worker.xhtml new file mode 100644 index 0000000000..dc181ce07d --- /dev/null +++ b/dom/system/tests/test_pathutils_worker.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<window title="Testing PathUtils 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> + <script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script> + + <script type="application/javascript"> + <![CDATA[ + function test() { + SimpleTest.waitForExplicitFinish(); + + info("test_pathtuils_worker.xhtml: Starting test"); + + const worker = new ChromeWorker("pathutils_worker.js"); + info("test_pathtuils_worker.xhtml: ChromeWorker created"); + + listenForTests(worker, { verbose: false }); + worker.postMessage({ + profileDir: PathUtils.profileDir, + localProfileDir: PathUtils.localProfileDir, + tempDir: PathUtils.tempDir, + }); + + info("test_pathtuils_worker.xhtml: Test running..."); + } + ]]> + </script> + <body xmlns="https://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> |