|
1 "use strict"; |
|
2 |
|
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"); |
|
7 |
|
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"); |
|
13 |
|
14 let myok = ok; |
|
15 let myis = is; |
|
16 let myinfo = info; |
|
17 let myisnot = isnot; |
|
18 |
|
19 let isPromise = function ispromise(value) { |
|
20 return value != null && typeof value == "object" && "then" in value; |
|
21 }; |
|
22 |
|
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 }; |
|
78 |
|
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 }; |
|
113 |
|
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 }; |
|
130 |
|
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 }; |
|
140 |
|
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 } |
|
148 |
|
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 }); |
|
166 |
|
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"); |
|
172 |
|
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 }); |
|
184 |
|
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 }); |
|
197 |
|
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; |
|
206 |
|
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 } |
|
215 |
|
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 }); |
|
226 |
|
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 }); |
|
249 |
|
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"); |
|
260 |
|
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"); |
|
266 |
|
267 let stat = yield fileSource.stat(); |
|
268 test.info("Input stat worked"); |
|
269 let size = stat.size; |
|
270 let array = new Uint8Array(size); |
|
271 |
|
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"); |
|
280 |
|
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 } |
|
295 |
|
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); |
|
299 |
|
300 // Cleanup. |
|
301 OS.File.remove(pathDest); |
|
302 }); |
|
303 }); |
|
304 |
|
305 |
|
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); |
|
312 |
|
313 try { |
|
314 let stat = yield file.stat(); |
|
315 test.info("Obtained file length"); |
|
316 |
|
317 let view = new Uint8Array(stat.size); |
|
318 yield file.readTo(view); |
|
319 test.info("First batch of content read"); |
|
320 |
|
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"); |
|
328 |
|
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 }); |
|
342 |
|
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(); |
|
349 |
|
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"); |
|
363 |
|
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 } |
|
378 |
|
379 yield iterator.close(); |
|
380 test.info("Closed iterator"); |
|
381 |
|
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"); |
|
387 |
|
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 } |
|
403 |
|
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(); |
|
408 |
|
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(); |
|
413 |
|
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(); |
|
429 |
|
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 } |
|
442 |
|
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 }); |
|
467 |
|
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 }); |
|
479 |
|
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 }); |
|
503 |
|
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 }); |
|
529 |
|
530 |