Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
1 "use strict";
3 Components.utils.import("resource://gre/modules/osfile.jsm");
4 Components.utils.import("resource://gre/modules/Promise.jsm");
5 Components.utils.import("resource://gre/modules/Task.jsm");
6 Components.utils.import("resource://gre/modules/AsyncShutdown.jsm");
8 // The following are used to compare against a well-tested reference
9 // implementation of file I/O.
10 Components.utils.import("resource://gre/modules/NetUtil.jsm");
11 Components.utils.import("resource://gre/modules/FileUtils.jsm");
12 Components.utils.import("resource://gre/modules/Services.jsm");
14 let myok = ok;
15 let myis = is;
16 let myinfo = info;
17 let myisnot = isnot;
19 let isPromise = function ispromise(value) {
20 return value != null && typeof value == "object" && "then" in value;
21 };
23 let maketest = function(prefix, test) {
24 let utils = {
25 ok: function ok(t, m) {
26 myok(t, prefix + ": " + m);
27 },
28 is: function is(l, r, m) {
29 myis(l, r, prefix + ": " + m);
30 },
31 isnot: function isnot(l, r, m) {
32 myisnot(l, r, prefix + ": " + m);
33 },
34 info: function info(m) {
35 myinfo(prefix + ": " + m);
36 },
37 fail: function fail(m) {
38 utils.ok(false, m);
39 },
40 okpromise: function okpromise(t, m) {
41 return t.then(
42 function onSuccess() {
43 util.ok(true, m);
44 },
45 function onFailure() {
46 util.ok(false, m);
47 }
48 );
49 }
50 };
51 return function runtest() {
52 utils.info("Entering");
53 try {
54 let result = test.call(this, utils);
55 if (!isPromise(result)) {
56 throw new TypeError("The test did not return a promise");
57 }
58 utils.info("This was a promise");
59 // The test returns a promise
60 result = result.then(function test_complete() {
61 utils.info("Complete");
62 }, function catch_uncaught_errors(err) {
63 utils.fail("Uncaught error " + err);
64 if (err && typeof err == "object" && "message" in err) {
65 utils.fail("(" + err.message + ")");
66 }
67 if (err && typeof err == "object" && "stack" in err) {
68 utils.fail("at " + err.stack);
69 }
70 });
71 return result;
72 } catch (x) {
73 utils.fail("Error " + x + " at " + x.stack);
74 return null;
75 }
76 };
77 };
79 /**
80 * Fetch asynchronously the contents of a file using xpcom.
81 *
82 * Used for comparing xpcom-based results to os.file-based results.
83 *
84 * @param {string} path The _absolute_ path to the file.
85 * @return {promise}
86 * @resolves {string} The contents of the file.
87 */
88 let reference_fetch_file = function reference_fetch_file(path, test) {
89 test.info("Fetching file " + path);
90 let promise = Promise.defer();
91 let file = new FileUtils.File(path);
92 NetUtil.asyncFetch(file,
93 function(stream, status) {
94 if (!Components.isSuccessCode(status)) {
95 promise.reject(status);
96 return;
97 }
98 let result, reject;
99 try {
100 result = NetUtil.readInputStreamToString(stream, stream.available());
101 } catch (x) {
102 reject = x;
103 }
104 stream.close();
105 if (reject) {
106 promise.reject(reject);
107 } else {
108 promise.resolve(result);
109 }
110 });
111 return promise.promise;
112 };
114 /**
115 * Compare asynchronously the contents two files using xpcom.
116 *
117 * Used for comparing xpcom-based results to os.file-based results.
118 *
119 * @param {string} a The _absolute_ path to the first file.
120 * @param {string} b The _absolute_ path to the second file.
121 *
122 * @resolves {null}
123 */
124 let reference_compare_files = function reference_compare_files(a, b, test) {
125 test.info("Comparing files " + a + " and " + b);
126 let a_contents = yield reference_fetch_file(a, test);
127 let b_contents = yield reference_fetch_file(b, test);
128 is(a_contents, b_contents, "Contents of files " + a + " and " + b + " match");
129 };
131 let reference_dir_contents = function reference_dir_contents(path) {
132 let result = [];
133 let entries = new FileUtils.File(path).directoryEntries;
134 while (entries.hasMoreElements()) {
135 let entry = entries.getNext().QueryInterface(Components.interfaces.nsILocalFile);
136 result.push(entry.path);
137 }
138 return result;
139 };
141 // Set/Unset OS.Shared.DEBUG, OS.Shared.TEST and a console listener.
142 function toggleDebugTest (pref, consoleListener) {
143 Services.prefs.setBoolPref("toolkit.osfile.log", pref);
144 Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref);
145 Services.console[pref ? "registerListener" : "unregisterListener"](
146 consoleListener);
147 }
149 let test = maketest("Main", function main(test) {
150 return Task.spawn(function() {
151 SimpleTest.waitForExplicitFinish();
152 yield test_constants();
153 yield test_path();
154 yield test_stat();
155 yield test_debug();
156 yield test_info_features_detect();
157 yield test_read_write();
158 yield test_position();
159 yield test_iter();
160 yield test_exists();
161 yield test_debug_test();
162 info("Test is over");
163 SimpleTest.finish();
164 });
165 });
167 /**
168 * A file that we know exists and that can be used for reading.
169 */
170 let EXISTING_FILE = OS.Path.join("chrome", "toolkit", "components",
171 "osfile", "tests", "mochi", "main_test_osfile_async.js");
173 /**
174 * Test that OS.Constants is defined correctly.
175 */
176 let test_constants = maketest("constants", function constants(test) {
177 return Task.spawn(function() {
178 test.isnot(OS.Constants, null, "OS.Constants exists");
179 test.ok(OS.Constants.Win || OS.Constants.libc, "OS.Constants.Win exists or OS.Constants.Unix exists");
180 test.isnot(OS.Constants.Path, null, "OS.Constants.Path exists");
181 test.isnot(OS.Constants.Sys, null, "OS.Constants.Sys exists");
182 });
183 });
185 /**
186 * Test that OS.Constants.Path paths are consistent.
187 */
188 let test_path = maketest("path", function path(test) {
189 return Task.spawn(function() {
190 test.ok(OS.Path, "OS.Path exists");
191 test.ok(OS.Constants.Path, "OS.Constants.Path exists");
192 test.is(OS.Constants.Path.tmpDir, Services.dirsvc.get("TmpD", Components.interfaces.nsIFile).path, "OS.Constants.Path.tmpDir is correct");
193 test.is(OS.Constants.Path.profileDir, Services.dirsvc.get("ProfD", Components.interfaces.nsIFile).path, "OS.Constants.Path.profileDir is correct");
194 test.is(OS.Constants.Path.localProfileDir, Services.dirsvc.get("ProfLD", Components.interfaces.nsIFile).path, "OS.Constants.Path.localProfileDir is correct");
195 });
196 });
198 /**
199 * Test OS.File.stat and OS.File.prototype.stat
200 */
201 let test_stat = maketest("stat", function stat(test) {
202 return Task.spawn(function() {
203 // Open a file and stat it
204 let file = yield OS.File.open(EXISTING_FILE);
205 let stat1;
207 try {
208 test.info("Stating file");
209 stat1 = yield file.stat();
210 test.ok(true, "stat has worked " + stat1);
211 test.ok(stat1, "stat is not empty");
212 } finally {
213 yield file.close();
214 }
216 // Stat the same file without opening it
217 test.info("Stating a file without opening it");
218 let stat2 = yield OS.File.stat(EXISTING_FILE);
219 test.ok(true, "stat 2 has worked " + stat2);
220 test.ok(stat2, "stat 2 is not empty");
221 for (let key in stat2) {
222 test.is("" + stat1[key], "" + stat2[key], "Stat field " + key + "is the same");
223 }
224 });
225 });
227 /**
228 * Test feature detection using OS.File.Info.prototype on main thread
229 */
230 let test_info_features_detect = maketest("features_detect", function features_detect(test) {
231 return Task.spawn(function() {
232 if (OS.Constants.Win) {
233 // see if winBirthDate is defined
234 if ("winBirthDate" in OS.File.Info.prototype) {
235 test.ok(true, "winBirthDate is defined");
236 } else {
237 test.fail("winBirthDate not defined though we are under Windows");
238 }
239 } else if (OS.Constants.libc) {
240 // see if unixGroup is defined
241 if ("unixGroup" in OS.File.Info.prototype) {
242 test.ok(true, "unixGroup is defined");
243 } else {
244 test.fail("unixGroup is not defined though we are under Unix");
245 }
246 }
247 });
248 });
250 /**
251 * Test OS.File.prototype.{read, readTo, write}
252 */
253 let test_read_write = maketest("read_write", function read_write(test) {
254 return Task.spawn(function() {
255 // Test readTo/write
256 let currentDir = yield OS.File.getCurrentDirectory();
257 let pathSource = OS.Path.join(currentDir, EXISTING_FILE);
258 let pathDest = OS.Path.join(OS.Constants.Path.tmpDir,
259 "osfile async test.tmp");
261 let fileSource = yield OS.File.open(pathSource);
262 test.info("Input file opened");
263 let fileDest = yield OS.File.open(pathDest,
264 { truncate: true, read: true, write: true});
265 test.info("Output file opened");
267 let stat = yield fileSource.stat();
268 test.info("Input stat worked");
269 let size = stat.size;
270 let array = new Uint8Array(size);
272 try {
273 test.info("Now calling readTo");
274 let readLength = yield fileSource.readTo(array);
275 test.info("ReadTo worked");
276 test.is(readLength, size, "ReadTo got all bytes");
277 let writeLength = yield fileDest.write(array);
278 test.info("Write worked");
279 test.is(writeLength, size, "Write wrote all bytes");
281 // Test read
282 yield fileSource.setPosition(0);
283 let readAllResult = yield fileSource.read();
284 test.info("ReadAll worked");
285 test.is(readAllResult.length, size, "ReadAll read all bytes");
286 test.is(Array.prototype.join.call(readAllResult),
287 Array.prototype.join.call(array),
288 "ReadAll result is correct");
289 } finally {
290 // Close stuff
291 yield fileSource.close();
292 yield fileDest.close();
293 test.info("Files are closed");
294 }
296 stat = yield OS.File.stat(pathDest);
297 test.is(stat.size, size, "Both files have the same size");
298 yield reference_compare_files(pathSource, pathDest, test);
300 // Cleanup.
301 OS.File.remove(pathDest);
302 });
303 });
306 /**
307 * Test file.{getPosition, setPosition}
308 */
309 let test_position = maketest("position", function position(test) {
310 return Task.spawn(function() {
311 let file = yield OS.File.open(EXISTING_FILE);
313 try {
314 let stat = yield file.stat();
315 test.info("Obtained file length");
317 let view = new Uint8Array(stat.size);
318 yield file.readTo(view);
319 test.info("First batch of content read");
321 let CHUNK_SIZE = 178;// An arbitrary number of bytes to read from the file
322 let pos = yield file.getPosition();
323 test.info("Obtained position");
324 test.is(pos, view.byteLength, "getPosition returned the end of the file");
325 pos = yield file.setPosition(-CHUNK_SIZE, OS.File.POS_END);
326 test.info("Changed position");
327 test.is(pos, view.byteLength - CHUNK_SIZE, "setPosition returned the correct position");
329 let view2 = new Uint8Array(CHUNK_SIZE);
330 yield file.readTo(view2);
331 test.info("Read the end of the file");
332 for (let i = 0; i < CHUNK_SIZE; ++i) {
333 if (view2[i] != view[i + view.byteLength - CHUNK_SIZE]) {
334 test.is(view2[i], view[i], "setPosition put us in the right position");
335 }
336 }
337 } finally {
338 yield file.close();
339 }
340 });
341 });
343 /**
344 * Test OS.File.prototype.{DirectoryIterator}
345 */
346 let test_iter = maketest("iter", function iter(test) {
347 return Task.spawn(function() {
348 let currentDir = yield OS.File.getCurrentDirectory();
350 // Trivial walks through the directory
351 test.info("Preparing iteration");
352 let iterator = new OS.File.DirectoryIterator(currentDir);
353 let temporary_file_name = OS.Path.join(currentDir, "empty-temporary-file.tmp");
354 try {
355 yield OS.File.remove(temporary_file_name);
356 } catch (err) {
357 // Ignore errors removing file
358 }
359 let allFiles1 = yield iterator.nextBatch();
360 test.info("Obtained all files through nextBatch");
361 test.isnot(allFiles1.length, 0, "There is at least one file");
362 test.isnot(allFiles1[0].path, null, "Files have a path");
364 // Ensure that we have the same entries with |reference_dir_contents|
365 let referenceEntries = new Set();
366 for (let entry of reference_dir_contents(currentDir)) {
367 referenceEntries.add(entry);
368 }
369 test.is(referenceEntries.size, allFiles1.length, "All the entries in the directory have been listed");
370 for (let entry of allFiles1) {
371 test.ok(referenceEntries.has(entry.path), "File " + entry.path + " effectively exists");
372 // Ensure that we have correct isDir and isSymLink
373 // Current directory is {objdir}/_tests/testing/mochitest/, assume it has some dirs and symlinks.
374 var f = new FileUtils.File(entry.path);
375 test.is(entry.isDir, f.isDirectory(), "Get file " + entry.path + " isDir correctly");
376 test.is(entry.isSymLink, f.isSymlink(), "Get file " + entry.path + " isSymLink correctly");
377 }
379 yield iterator.close();
380 test.info("Closed iterator");
382 test.info("Double closing DirectoryIterator");
383 iterator = new OS.File.DirectoryIterator(currentDir);
384 yield iterator.close();
385 yield iterator.close(); //double closing |DirectoryIterator|
386 test.ok(true, "|DirectoryIterator| was closed twice successfully");
388 let allFiles2 = [];
389 let i = 0;
390 iterator = new OS.File.DirectoryIterator(currentDir);
391 yield iterator.forEach(function(entry, index) {
392 test.is(i++, index, "Getting the correct index");
393 allFiles2.push(entry);
394 });
395 test.info("Obtained all files through forEach");
396 is(allFiles1.length, allFiles2.length, "Both runs returned the same number of files");
397 for (let i = 0; i < allFiles1.length; ++i) {
398 if (allFiles1[i].path != allFiles2[i].path) {
399 test.is(allFiles1[i].path, allFiles2[i].path, "Both runs return the same files");
400 break;
401 }
402 }
404 // Testing batch iteration + whether an iteration can be stopped early
405 let BATCH_LENGTH = 10;
406 test.info("Getting some files through nextBatch");
407 yield iterator.close();
409 iterator = new OS.File.DirectoryIterator(currentDir);
410 let someFiles1 = yield iterator.nextBatch(BATCH_LENGTH);
411 let someFiles2 = yield iterator.nextBatch(BATCH_LENGTH);
412 yield iterator.close();
414 iterator = new OS.File.DirectoryIterator(currentDir);
415 yield iterator.forEach(function cb(entry, index, iterator) {
416 if (index < BATCH_LENGTH) {
417 test.is(entry.path, someFiles1[index].path, "Both runs return the same files (part 1)");
418 } else if (index < 2*BATCH_LENGTH) {
419 test.is(entry.path, someFiles2[index - BATCH_LENGTH].path, "Both runs return the same files (part 2)");
420 } else if (index == 2 * BATCH_LENGTH) {
421 test.info("Attempting to stop asynchronous forEach");
422 return iterator.close();
423 } else {
424 test.fail("Can we stop an asynchronous forEach? " + index);
425 }
426 return null;
427 });
428 yield iterator.close();
430 // Ensuring that we find new files if they appear
431 let file = yield OS.File.open(temporary_file_name, { write: true } );
432 file.close();
433 iterator = new OS.File.DirectoryIterator(currentDir);
434 try {
435 let files = yield iterator.nextBatch();
436 is(files.length, allFiles1.length + 1, "The directory iterator has noticed the new file");
437 let exists = yield iterator.exists();
438 test.ok(exists, "After nextBatch, iterator detects that the directory exists");
439 } finally {
440 yield iterator.close();
441 }
443 // Ensuring that opening a non-existing directory fails consistently
444 // once iteration starts.
445 try {
446 iterator = null;
447 iterator = new OS.File.DirectoryIterator("/I do not exist");
448 let exists = yield iterator.exists();
449 test.ok(!exists, "Before any iteration, iterator detects that the directory doesn't exist");
450 let exn = null;
451 try {
452 yield iterator.next();
453 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
454 exn = ex;
455 let exists = yield iterator.exists();
456 test.ok(!exists, "After one iteration, iterator detects that the directory doesn't exist");
457 }
458 test.ok(exn, "Iterating through a directory that does not exist has failed with becauseNoSuchFile");
459 } finally {
460 if (iterator) {
461 iterator.close();
462 }
463 }
464 test.ok(!!iterator, "The directory iterator for a non-existing directory was correctly created");
465 });
466 });
468 /**
469 * Test OS.File.prototype.{exists}
470 */
471 let test_exists = maketest("exists", function exists(test) {
472 return Task.spawn(function() {
473 let fileExists = yield OS.File.exists(EXISTING_FILE);
474 test.ok(fileExists, "file exists");
475 fileExists = yield OS.File.exists(EXISTING_FILE + ".tmp");
476 test.ok(!fileExists, "file does not exists");
477 });
478 });
480 /**
481 * Test changes to OS.Shared.DEBUG flag.
482 */
483 let test_debug = maketest("debug", function debug(test) {
484 return Task.spawn(function() {
485 function testSetDebugPref (pref) {
486 try {
487 Services.prefs.setBoolPref("toolkit.osfile.log", pref);
488 } catch (x) {
489 test.fail("Setting OS.Shared.DEBUG to " + pref +
490 " should not cause error.");
491 } finally {
492 test.is(OS.Shared.DEBUG, pref, "OS.Shared.DEBUG is set correctly.");
493 }
494 }
495 testSetDebugPref(true);
496 let workerDEBUG = yield OS.File.GET_DEBUG();
497 test.is(workerDEBUG, true, "Worker's DEBUG is set.");
498 testSetDebugPref(false);
499 workerDEBUG = yield OS.File.GET_DEBUG();
500 test.is(workerDEBUG, false, "Worker's DEBUG is unset.");
501 });
502 });
504 /**
505 * Test logging in the main thread with set OS.Shared.DEBUG and
506 * OS.Shared.TEST flags.
507 */
508 let test_debug_test = maketest("debug_test", function debug_test(test) {
509 return Task.spawn(function () {
510 // Create a console listener.
511 let consoleListener = {
512 observe: function (aMessage) {
513 // Ignore unexpected messages.
514 if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) {
515 return;
516 }
517 if (aMessage.message.indexOf("TEST OS") < 0) {
518 return;
519 }
520 test.ok(true, "DEBUG TEST messages are logged correctly.");
521 }
522 };
523 toggleDebugTest(true, consoleListener);
524 // Execution of OS.File.exist method will trigger OS.File.LOG several times.
525 let fileExists = yield OS.File.exists(EXISTING_FILE);
526 toggleDebugTest(false, consoleListener);
527 });
528 });