michael@0: "use strict"; michael@0: michael@0: Components.utils.import("resource://gre/modules/osfile.jsm"); michael@0: Components.utils.import("resource://gre/modules/Promise.jsm"); michael@0: Components.utils.import("resource://gre/modules/Task.jsm"); michael@0: Components.utils.import("resource://gre/modules/AsyncShutdown.jsm"); michael@0: michael@0: // The following are used to compare against a well-tested reference michael@0: // implementation of file I/O. michael@0: Components.utils.import("resource://gre/modules/NetUtil.jsm"); michael@0: Components.utils.import("resource://gre/modules/FileUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: let myok = ok; michael@0: let myis = is; michael@0: let myinfo = info; michael@0: let myisnot = isnot; michael@0: michael@0: let isPromise = function ispromise(value) { michael@0: return value != null && typeof value == "object" && "then" in value; michael@0: }; michael@0: michael@0: let maketest = function(prefix, test) { michael@0: let utils = { michael@0: ok: function ok(t, m) { michael@0: myok(t, prefix + ": " + m); michael@0: }, michael@0: is: function is(l, r, m) { michael@0: myis(l, r, prefix + ": " + m); michael@0: }, michael@0: isnot: function isnot(l, r, m) { michael@0: myisnot(l, r, prefix + ": " + m); michael@0: }, michael@0: info: function info(m) { michael@0: myinfo(prefix + ": " + m); michael@0: }, michael@0: fail: function fail(m) { michael@0: utils.ok(false, m); michael@0: }, michael@0: okpromise: function okpromise(t, m) { michael@0: return t.then( michael@0: function onSuccess() { michael@0: util.ok(true, m); michael@0: }, michael@0: function onFailure() { michael@0: util.ok(false, m); michael@0: } michael@0: ); michael@0: } michael@0: }; michael@0: return function runtest() { michael@0: utils.info("Entering"); michael@0: try { michael@0: let result = test.call(this, utils); michael@0: if (!isPromise(result)) { michael@0: throw new TypeError("The test did not return a promise"); michael@0: } michael@0: utils.info("This was a promise"); michael@0: // The test returns a promise michael@0: result = result.then(function test_complete() { michael@0: utils.info("Complete"); michael@0: }, function catch_uncaught_errors(err) { michael@0: utils.fail("Uncaught error " + err); michael@0: if (err && typeof err == "object" && "message" in err) { michael@0: utils.fail("(" + err.message + ")"); michael@0: } michael@0: if (err && typeof err == "object" && "stack" in err) { michael@0: utils.fail("at " + err.stack); michael@0: } michael@0: }); michael@0: return result; michael@0: } catch (x) { michael@0: utils.fail("Error " + x + " at " + x.stack); michael@0: return null; michael@0: } michael@0: }; michael@0: }; michael@0: michael@0: /** michael@0: * Fetch asynchronously the contents of a file using xpcom. michael@0: * michael@0: * Used for comparing xpcom-based results to os.file-based results. michael@0: * michael@0: * @param {string} path The _absolute_ path to the file. michael@0: * @return {promise} michael@0: * @resolves {string} The contents of the file. michael@0: */ michael@0: let reference_fetch_file = function reference_fetch_file(path, test) { michael@0: test.info("Fetching file " + path); michael@0: let promise = Promise.defer(); michael@0: let file = new FileUtils.File(path); michael@0: NetUtil.asyncFetch(file, michael@0: function(stream, status) { michael@0: if (!Components.isSuccessCode(status)) { michael@0: promise.reject(status); michael@0: return; michael@0: } michael@0: let result, reject; michael@0: try { michael@0: result = NetUtil.readInputStreamToString(stream, stream.available()); michael@0: } catch (x) { michael@0: reject = x; michael@0: } michael@0: stream.close(); michael@0: if (reject) { michael@0: promise.reject(reject); michael@0: } else { michael@0: promise.resolve(result); michael@0: } michael@0: }); michael@0: return promise.promise; michael@0: }; michael@0: michael@0: /** michael@0: * Compare asynchronously the contents two files using xpcom. michael@0: * michael@0: * Used for comparing xpcom-based results to os.file-based results. michael@0: * michael@0: * @param {string} a The _absolute_ path to the first file. michael@0: * @param {string} b The _absolute_ path to the second file. michael@0: * michael@0: * @resolves {null} michael@0: */ michael@0: let reference_compare_files = function reference_compare_files(a, b, test) { michael@0: test.info("Comparing files " + a + " and " + b); michael@0: let a_contents = yield reference_fetch_file(a, test); michael@0: let b_contents = yield reference_fetch_file(b, test); michael@0: is(a_contents, b_contents, "Contents of files " + a + " and " + b + " match"); michael@0: }; michael@0: michael@0: let reference_dir_contents = function reference_dir_contents(path) { michael@0: let result = []; michael@0: let entries = new FileUtils.File(path).directoryEntries; michael@0: while (entries.hasMoreElements()) { michael@0: let entry = entries.getNext().QueryInterface(Components.interfaces.nsILocalFile); michael@0: result.push(entry.path); michael@0: } michael@0: return result; michael@0: }; michael@0: michael@0: // Set/Unset OS.Shared.DEBUG, OS.Shared.TEST and a console listener. michael@0: function toggleDebugTest (pref, consoleListener) { michael@0: Services.prefs.setBoolPref("toolkit.osfile.log", pref); michael@0: Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref); michael@0: Services.console[pref ? "registerListener" : "unregisterListener"]( michael@0: consoleListener); michael@0: } michael@0: michael@0: let test = maketest("Main", function main(test) { michael@0: return Task.spawn(function() { michael@0: SimpleTest.waitForExplicitFinish(); michael@0: yield test_constants(); michael@0: yield test_path(); michael@0: yield test_stat(); michael@0: yield test_debug(); michael@0: yield test_info_features_detect(); michael@0: yield test_read_write(); michael@0: yield test_position(); michael@0: yield test_iter(); michael@0: yield test_exists(); michael@0: yield test_debug_test(); michael@0: info("Test is over"); michael@0: SimpleTest.finish(); michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * A file that we know exists and that can be used for reading. michael@0: */ michael@0: let EXISTING_FILE = OS.Path.join("chrome", "toolkit", "components", michael@0: "osfile", "tests", "mochi", "main_test_osfile_async.js"); michael@0: michael@0: /** michael@0: * Test that OS.Constants is defined correctly. michael@0: */ michael@0: let test_constants = maketest("constants", function constants(test) { michael@0: return Task.spawn(function() { michael@0: test.isnot(OS.Constants, null, "OS.Constants exists"); michael@0: test.ok(OS.Constants.Win || OS.Constants.libc, "OS.Constants.Win exists or OS.Constants.Unix exists"); michael@0: test.isnot(OS.Constants.Path, null, "OS.Constants.Path exists"); michael@0: test.isnot(OS.Constants.Sys, null, "OS.Constants.Sys exists"); michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test that OS.Constants.Path paths are consistent. michael@0: */ michael@0: let test_path = maketest("path", function path(test) { michael@0: return Task.spawn(function() { michael@0: test.ok(OS.Path, "OS.Path exists"); michael@0: test.ok(OS.Constants.Path, "OS.Constants.Path exists"); michael@0: test.is(OS.Constants.Path.tmpDir, Services.dirsvc.get("TmpD", Components.interfaces.nsIFile).path, "OS.Constants.Path.tmpDir is correct"); michael@0: test.is(OS.Constants.Path.profileDir, Services.dirsvc.get("ProfD", Components.interfaces.nsIFile).path, "OS.Constants.Path.profileDir is correct"); michael@0: test.is(OS.Constants.Path.localProfileDir, Services.dirsvc.get("ProfLD", Components.interfaces.nsIFile).path, "OS.Constants.Path.localProfileDir is correct"); michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test OS.File.stat and OS.File.prototype.stat michael@0: */ michael@0: let test_stat = maketest("stat", function stat(test) { michael@0: return Task.spawn(function() { michael@0: // Open a file and stat it michael@0: let file = yield OS.File.open(EXISTING_FILE); michael@0: let stat1; michael@0: michael@0: try { michael@0: test.info("Stating file"); michael@0: stat1 = yield file.stat(); michael@0: test.ok(true, "stat has worked " + stat1); michael@0: test.ok(stat1, "stat is not empty"); michael@0: } finally { michael@0: yield file.close(); michael@0: } michael@0: michael@0: // Stat the same file without opening it michael@0: test.info("Stating a file without opening it"); michael@0: let stat2 = yield OS.File.stat(EXISTING_FILE); michael@0: test.ok(true, "stat 2 has worked " + stat2); michael@0: test.ok(stat2, "stat 2 is not empty"); michael@0: for (let key in stat2) { michael@0: test.is("" + stat1[key], "" + stat2[key], "Stat field " + key + "is the same"); michael@0: } michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test feature detection using OS.File.Info.prototype on main thread michael@0: */ michael@0: let test_info_features_detect = maketest("features_detect", function features_detect(test) { michael@0: return Task.spawn(function() { michael@0: if (OS.Constants.Win) { michael@0: // see if winBirthDate is defined michael@0: if ("winBirthDate" in OS.File.Info.prototype) { michael@0: test.ok(true, "winBirthDate is defined"); michael@0: } else { michael@0: test.fail("winBirthDate not defined though we are under Windows"); michael@0: } michael@0: } else if (OS.Constants.libc) { michael@0: // see if unixGroup is defined michael@0: if ("unixGroup" in OS.File.Info.prototype) { michael@0: test.ok(true, "unixGroup is defined"); michael@0: } else { michael@0: test.fail("unixGroup is not defined though we are under Unix"); michael@0: } michael@0: } michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test OS.File.prototype.{read, readTo, write} michael@0: */ michael@0: let test_read_write = maketest("read_write", function read_write(test) { michael@0: return Task.spawn(function() { michael@0: // Test readTo/write michael@0: let currentDir = yield OS.File.getCurrentDirectory(); michael@0: let pathSource = OS.Path.join(currentDir, EXISTING_FILE); michael@0: let pathDest = OS.Path.join(OS.Constants.Path.tmpDir, michael@0: "osfile async test.tmp"); michael@0: michael@0: let fileSource = yield OS.File.open(pathSource); michael@0: test.info("Input file opened"); michael@0: let fileDest = yield OS.File.open(pathDest, michael@0: { truncate: true, read: true, write: true}); michael@0: test.info("Output file opened"); michael@0: michael@0: let stat = yield fileSource.stat(); michael@0: test.info("Input stat worked"); michael@0: let size = stat.size; michael@0: let array = new Uint8Array(size); michael@0: michael@0: try { michael@0: test.info("Now calling readTo"); michael@0: let readLength = yield fileSource.readTo(array); michael@0: test.info("ReadTo worked"); michael@0: test.is(readLength, size, "ReadTo got all bytes"); michael@0: let writeLength = yield fileDest.write(array); michael@0: test.info("Write worked"); michael@0: test.is(writeLength, size, "Write wrote all bytes"); michael@0: michael@0: // Test read michael@0: yield fileSource.setPosition(0); michael@0: let readAllResult = yield fileSource.read(); michael@0: test.info("ReadAll worked"); michael@0: test.is(readAllResult.length, size, "ReadAll read all bytes"); michael@0: test.is(Array.prototype.join.call(readAllResult), michael@0: Array.prototype.join.call(array), michael@0: "ReadAll result is correct"); michael@0: } finally { michael@0: // Close stuff michael@0: yield fileSource.close(); michael@0: yield fileDest.close(); michael@0: test.info("Files are closed"); michael@0: } michael@0: michael@0: stat = yield OS.File.stat(pathDest); michael@0: test.is(stat.size, size, "Both files have the same size"); michael@0: yield reference_compare_files(pathSource, pathDest, test); michael@0: michael@0: // Cleanup. michael@0: OS.File.remove(pathDest); michael@0: }); michael@0: }); michael@0: michael@0: michael@0: /** michael@0: * Test file.{getPosition, setPosition} michael@0: */ michael@0: let test_position = maketest("position", function position(test) { michael@0: return Task.spawn(function() { michael@0: let file = yield OS.File.open(EXISTING_FILE); michael@0: michael@0: try { michael@0: let stat = yield file.stat(); michael@0: test.info("Obtained file length"); michael@0: michael@0: let view = new Uint8Array(stat.size); michael@0: yield file.readTo(view); michael@0: test.info("First batch of content read"); michael@0: michael@0: let CHUNK_SIZE = 178;// An arbitrary number of bytes to read from the file michael@0: let pos = yield file.getPosition(); michael@0: test.info("Obtained position"); michael@0: test.is(pos, view.byteLength, "getPosition returned the end of the file"); michael@0: pos = yield file.setPosition(-CHUNK_SIZE, OS.File.POS_END); michael@0: test.info("Changed position"); michael@0: test.is(pos, view.byteLength - CHUNK_SIZE, "setPosition returned the correct position"); michael@0: michael@0: let view2 = new Uint8Array(CHUNK_SIZE); michael@0: yield file.readTo(view2); michael@0: test.info("Read the end of the file"); michael@0: for (let i = 0; i < CHUNK_SIZE; ++i) { michael@0: if (view2[i] != view[i + view.byteLength - CHUNK_SIZE]) { michael@0: test.is(view2[i], view[i], "setPosition put us in the right position"); michael@0: } michael@0: } michael@0: } finally { michael@0: yield file.close(); michael@0: } michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test OS.File.prototype.{DirectoryIterator} michael@0: */ michael@0: let test_iter = maketest("iter", function iter(test) { michael@0: return Task.spawn(function() { michael@0: let currentDir = yield OS.File.getCurrentDirectory(); michael@0: michael@0: // Trivial walks through the directory michael@0: test.info("Preparing iteration"); michael@0: let iterator = new OS.File.DirectoryIterator(currentDir); michael@0: let temporary_file_name = OS.Path.join(currentDir, "empty-temporary-file.tmp"); michael@0: try { michael@0: yield OS.File.remove(temporary_file_name); michael@0: } catch (err) { michael@0: // Ignore errors removing file michael@0: } michael@0: let allFiles1 = yield iterator.nextBatch(); michael@0: test.info("Obtained all files through nextBatch"); michael@0: test.isnot(allFiles1.length, 0, "There is at least one file"); michael@0: test.isnot(allFiles1[0].path, null, "Files have a path"); michael@0: michael@0: // Ensure that we have the same entries with |reference_dir_contents| michael@0: let referenceEntries = new Set(); michael@0: for (let entry of reference_dir_contents(currentDir)) { michael@0: referenceEntries.add(entry); michael@0: } michael@0: test.is(referenceEntries.size, allFiles1.length, "All the entries in the directory have been listed"); michael@0: for (let entry of allFiles1) { michael@0: test.ok(referenceEntries.has(entry.path), "File " + entry.path + " effectively exists"); michael@0: // Ensure that we have correct isDir and isSymLink michael@0: // Current directory is {objdir}/_tests/testing/mochitest/, assume it has some dirs and symlinks. michael@0: var f = new FileUtils.File(entry.path); michael@0: test.is(entry.isDir, f.isDirectory(), "Get file " + entry.path + " isDir correctly"); michael@0: test.is(entry.isSymLink, f.isSymlink(), "Get file " + entry.path + " isSymLink correctly"); michael@0: } michael@0: michael@0: yield iterator.close(); michael@0: test.info("Closed iterator"); michael@0: michael@0: test.info("Double closing DirectoryIterator"); michael@0: iterator = new OS.File.DirectoryIterator(currentDir); michael@0: yield iterator.close(); michael@0: yield iterator.close(); //double closing |DirectoryIterator| michael@0: test.ok(true, "|DirectoryIterator| was closed twice successfully"); michael@0: michael@0: let allFiles2 = []; michael@0: let i = 0; michael@0: iterator = new OS.File.DirectoryIterator(currentDir); michael@0: yield iterator.forEach(function(entry, index) { michael@0: test.is(i++, index, "Getting the correct index"); michael@0: allFiles2.push(entry); michael@0: }); michael@0: test.info("Obtained all files through forEach"); michael@0: is(allFiles1.length, allFiles2.length, "Both runs returned the same number of files"); michael@0: for (let i = 0; i < allFiles1.length; ++i) { michael@0: if (allFiles1[i].path != allFiles2[i].path) { michael@0: test.is(allFiles1[i].path, allFiles2[i].path, "Both runs return the same files"); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // Testing batch iteration + whether an iteration can be stopped early michael@0: let BATCH_LENGTH = 10; michael@0: test.info("Getting some files through nextBatch"); michael@0: yield iterator.close(); michael@0: michael@0: iterator = new OS.File.DirectoryIterator(currentDir); michael@0: let someFiles1 = yield iterator.nextBatch(BATCH_LENGTH); michael@0: let someFiles2 = yield iterator.nextBatch(BATCH_LENGTH); michael@0: yield iterator.close(); michael@0: michael@0: iterator = new OS.File.DirectoryIterator(currentDir); michael@0: yield iterator.forEach(function cb(entry, index, iterator) { michael@0: if (index < BATCH_LENGTH) { michael@0: test.is(entry.path, someFiles1[index].path, "Both runs return the same files (part 1)"); michael@0: } else if (index < 2*BATCH_LENGTH) { michael@0: test.is(entry.path, someFiles2[index - BATCH_LENGTH].path, "Both runs return the same files (part 2)"); michael@0: } else if (index == 2 * BATCH_LENGTH) { michael@0: test.info("Attempting to stop asynchronous forEach"); michael@0: return iterator.close(); michael@0: } else { michael@0: test.fail("Can we stop an asynchronous forEach? " + index); michael@0: } michael@0: return null; michael@0: }); michael@0: yield iterator.close(); michael@0: michael@0: // Ensuring that we find new files if they appear michael@0: let file = yield OS.File.open(temporary_file_name, { write: true } ); michael@0: file.close(); michael@0: iterator = new OS.File.DirectoryIterator(currentDir); michael@0: try { michael@0: let files = yield iterator.nextBatch(); michael@0: is(files.length, allFiles1.length + 1, "The directory iterator has noticed the new file"); michael@0: let exists = yield iterator.exists(); michael@0: test.ok(exists, "After nextBatch, iterator detects that the directory exists"); michael@0: } finally { michael@0: yield iterator.close(); michael@0: } michael@0: michael@0: // Ensuring that opening a non-existing directory fails consistently michael@0: // once iteration starts. michael@0: try { michael@0: iterator = null; michael@0: iterator = new OS.File.DirectoryIterator("/I do not exist"); michael@0: let exists = yield iterator.exists(); michael@0: test.ok(!exists, "Before any iteration, iterator detects that the directory doesn't exist"); michael@0: let exn = null; michael@0: try { michael@0: yield iterator.next(); michael@0: } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { michael@0: exn = ex; michael@0: let exists = yield iterator.exists(); michael@0: test.ok(!exists, "After one iteration, iterator detects that the directory doesn't exist"); michael@0: } michael@0: test.ok(exn, "Iterating through a directory that does not exist has failed with becauseNoSuchFile"); michael@0: } finally { michael@0: if (iterator) { michael@0: iterator.close(); michael@0: } michael@0: } michael@0: test.ok(!!iterator, "The directory iterator for a non-existing directory was correctly created"); michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test OS.File.prototype.{exists} michael@0: */ michael@0: let test_exists = maketest("exists", function exists(test) { michael@0: return Task.spawn(function() { michael@0: let fileExists = yield OS.File.exists(EXISTING_FILE); michael@0: test.ok(fileExists, "file exists"); michael@0: fileExists = yield OS.File.exists(EXISTING_FILE + ".tmp"); michael@0: test.ok(!fileExists, "file does not exists"); michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test changes to OS.Shared.DEBUG flag. michael@0: */ michael@0: let test_debug = maketest("debug", function debug(test) { michael@0: return Task.spawn(function() { michael@0: function testSetDebugPref (pref) { michael@0: try { michael@0: Services.prefs.setBoolPref("toolkit.osfile.log", pref); michael@0: } catch (x) { michael@0: test.fail("Setting OS.Shared.DEBUG to " + pref + michael@0: " should not cause error."); michael@0: } finally { michael@0: test.is(OS.Shared.DEBUG, pref, "OS.Shared.DEBUG is set correctly."); michael@0: } michael@0: } michael@0: testSetDebugPref(true); michael@0: let workerDEBUG = yield OS.File.GET_DEBUG(); michael@0: test.is(workerDEBUG, true, "Worker's DEBUG is set."); michael@0: testSetDebugPref(false); michael@0: workerDEBUG = yield OS.File.GET_DEBUG(); michael@0: test.is(workerDEBUG, false, "Worker's DEBUG is unset."); michael@0: }); michael@0: }); michael@0: michael@0: /** michael@0: * Test logging in the main thread with set OS.Shared.DEBUG and michael@0: * OS.Shared.TEST flags. michael@0: */ michael@0: let test_debug_test = maketest("debug_test", function debug_test(test) { michael@0: return Task.spawn(function () { michael@0: // Create a console listener. michael@0: let consoleListener = { michael@0: observe: function (aMessage) { michael@0: // Ignore unexpected messages. michael@0: if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) { michael@0: return; michael@0: } michael@0: if (aMessage.message.indexOf("TEST OS") < 0) { michael@0: return; michael@0: } michael@0: test.ok(true, "DEBUG TEST messages are logged correctly."); michael@0: } michael@0: }; michael@0: toggleDebugTest(true, consoleListener); michael@0: // Execution of OS.File.exist method will trigger OS.File.LOG several times. michael@0: let fileExists = yield OS.File.exists(EXISTING_FILE); michael@0: toggleDebugTest(false, consoleListener); michael@0: }); michael@0: }); michael@0: michael@0: