diff options
Diffstat (limited to '')
19 files changed, 2794 insertions, 0 deletions
diff --git a/dom/system/tests/ioutils/chrome.ini b/dom/system/tests/ioutils/chrome.ini new file mode 100644 index 0000000000..a961f45631 --- /dev/null +++ b/dom/system/tests/ioutils/chrome.ini @@ -0,0 +1,23 @@ +[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_stat_set_modification_time.html] +[test_ioutils_worker.xhtml] +[test_ioutils_set_permissions.html] +[test_ioutils_windows_file_attributes.html] +skip-if = (os != 'win') 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..9f9dfc899b --- /dev/null +++ b/dom/system/tests/ioutils/file_ioutils_worker.js @@ -0,0 +1,102 @@ +// Any copyright is dedicated to the Public Domain. +// - http://creativecommons.org/publicdomain/zero/1.0/ + +/* eslint-env mozilla/chrome-worker, node */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); +/* import-globals-from /toolkit/modules/ObjectUtils.jsm */ +importScripts("resource://gre/modules/ObjectUtils.jsm"); + +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( + ObjectUtils.deepEqual(bytes, fileContents) && + bytes.length == fileContents.length, + "IOUtils::read can read back entire file" + ); + + const tooManyBytes = bytes.length + 1; + fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes }); + ok( + ObjectUtils.deepEqual(bytes, fileContents) && + fileContents.length == bytes.length, + "IOUtils::read can read entire file when requested maxBytes is too large" + ); + + await cleanup(tmpFileName); + } + + async function test_move_file() { + const src = 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); + } +}; 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..1c27a28822 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html @@ -0,0 +1,54 @@ + +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + + 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 = [ + "22596363b3de40b06f981fb85d82312e8c0ed511", + "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", + "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80", + "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593", + ]; + const ALGORITHMS = ["sha1", "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..ba55eb8463 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html @@ -0,0 +1,359 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + 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..e447964343 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_create_unique.html @@ -0,0 +1,88 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + + 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..93e2c60039 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html @@ -0,0 +1,94 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + + 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..d5295f469f --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_getfile.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> + "use strict"; + + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + 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..cd6e7aeb5d --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html @@ -0,0 +1,90 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); + + 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..c1a073dea6 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html @@ -0,0 +1,133 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + + 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..f52115d261 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write.html @@ -0,0 +1,520 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + 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..e356e50c47 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html @@ -0,0 +1,165 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import( + "resource://gre/modules/ObjectUtils.jsm" + ); + + const OBJECT = { + "foo": [ + "bar", + 123, + 456.789, + true, + false, + null, + ], + "bar": { + "baz": {}, + }, + }; + + const ARRAY = [1, 2.3, true, false, null, { "foo": "bar" }]; + + const PRIMITIVES = [123, true, false, "hello, world", null]; + + add_task(async function read_json() { + const 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); + }); + </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..196f5d4862 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html @@ -0,0 +1,359 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + // This is 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); + }); + </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..85ffa481a4 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_remove.html @@ -0,0 +1,117 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + 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..8f8328bd81 --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html @@ -0,0 +1,241 @@ +<!-- Any copyright is dedicated to the Public Domain. +- http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test the IOUtils file I/O API</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="file_ioutils_test_fixtures.js"></script> + <script> + "use strict"; + + const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + + 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..b452b4f6db --- /dev/null +++ b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.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.import("resource://testing-common/Assert.jsm"); + + 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> |