entry.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. // Copyright 2011 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Mock filesystem objects. These are all in the same file to
  16. * avoid circular dependency issues.
  17. *
  18. */
  19. goog.setTestOnly('goog.testing.fs.DirectoryEntry');
  20. goog.provide('goog.testing.fs.DirectoryEntry');
  21. goog.provide('goog.testing.fs.Entry');
  22. goog.provide('goog.testing.fs.FileEntry');
  23. goog.require('goog.Timer');
  24. goog.require('goog.array');
  25. goog.require('goog.asserts');
  26. goog.require('goog.async.Deferred');
  27. goog.require('goog.fs.DirectoryEntry');
  28. goog.require('goog.fs.DirectoryEntryImpl');
  29. goog.require('goog.fs.Entry');
  30. goog.require('goog.fs.Error');
  31. goog.require('goog.fs.FileEntry');
  32. goog.require('goog.functions');
  33. goog.require('goog.object');
  34. goog.require('goog.string');
  35. goog.require('goog.testing.fs.File');
  36. goog.require('goog.testing.fs.FileWriter');
  37. /**
  38. * A mock filesystem entry object.
  39. *
  40. * @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
  41. * @param {!goog.testing.fs.DirectoryEntry} parent The directory entry directly
  42. * containing this entry.
  43. * @param {string} name The name of this entry.
  44. * @constructor
  45. * @implements {goog.fs.Entry}
  46. */
  47. goog.testing.fs.Entry = function(fs, parent, name) {
  48. /**
  49. * This entry's filesystem.
  50. * @type {!goog.testing.fs.FileSystem}
  51. * @private
  52. */
  53. this.fs_ = fs;
  54. /**
  55. * The name of this entry.
  56. * @type {string}
  57. * @private
  58. */
  59. this.name_ = name;
  60. /**
  61. * The parent of this entry.
  62. * @type {!goog.testing.fs.DirectoryEntry}
  63. */
  64. this.parent = parent;
  65. };
  66. /**
  67. * Whether or not this entry has been deleted.
  68. * @type {boolean}
  69. */
  70. goog.testing.fs.Entry.prototype.deleted = false;
  71. /** @override */
  72. goog.testing.fs.Entry.prototype.isFile = goog.abstractMethod;
  73. /** @override */
  74. goog.testing.fs.Entry.prototype.isDirectory = goog.abstractMethod;
  75. /** @override */
  76. goog.testing.fs.Entry.prototype.getName = function() {
  77. return this.name_;
  78. };
  79. /** @override */
  80. goog.testing.fs.Entry.prototype.getFullPath = function() {
  81. if (this.getName() == '' || this.parent.getName() == '') {
  82. // The root directory has an empty name
  83. return '/' + this.name_;
  84. } else {
  85. return this.parent.getFullPath() + '/' + this.name_;
  86. }
  87. };
  88. /**
  89. * @return {!goog.testing.fs.FileSystem}
  90. * @override
  91. */
  92. goog.testing.fs.Entry.prototype.getFileSystem = function() {
  93. return this.fs_;
  94. };
  95. /** @override */
  96. goog.testing.fs.Entry.prototype.getLastModified = goog.abstractMethod;
  97. /** @override */
  98. goog.testing.fs.Entry.prototype.getMetadata = goog.abstractMethod;
  99. /** @override */
  100. goog.testing.fs.Entry.prototype.moveTo = function(parent, opt_newName) {
  101. var msg = 'moving ' + this.getFullPath() + ' into ' + parent.getFullPath() +
  102. (opt_newName ? ', renaming to ' + opt_newName : '');
  103. var newFile;
  104. return this.checkNotDeleted(msg)
  105. .addCallback(function() { return this.copyTo(parent, opt_newName); })
  106. .addCallback(function(file) {
  107. newFile = file;
  108. return this.remove();
  109. })
  110. .addCallback(function() { return newFile; });
  111. };
  112. /** @override */
  113. goog.testing.fs.Entry.prototype.copyTo = function(parent, opt_newName) {
  114. goog.asserts.assert(parent instanceof goog.testing.fs.DirectoryEntry);
  115. var msg = 'copying ' + this.getFullPath() + ' into ' + parent.getFullPath() +
  116. (opt_newName ? ', renaming to ' + opt_newName : '');
  117. var self = this;
  118. return this.checkNotDeleted(msg).addCallback(function() {
  119. var name = opt_newName || self.getName();
  120. var entry = self.clone();
  121. /** @type {!goog.testing.fs.DirectoryEntry} */ (parent).children[name] =
  122. entry;
  123. parent.lastModifiedTimestamp_ = goog.now();
  124. entry.name_ = name;
  125. entry.parent = /** @type {!goog.testing.fs.DirectoryEntry} */ (parent);
  126. return entry;
  127. });
  128. };
  129. /**
  130. * @return {!goog.testing.fs.Entry} A shallow copy of this entry object.
  131. */
  132. goog.testing.fs.Entry.prototype.clone = goog.abstractMethod;
  133. /** @override */
  134. goog.testing.fs.Entry.prototype.toUrl = function(opt_mimetype) {
  135. return 'fakefilesystem:' + this.getFullPath();
  136. };
  137. /** @override */
  138. goog.testing.fs.Entry.prototype.toUri = goog.testing.fs.Entry.prototype.toUrl;
  139. /** @override */
  140. goog.testing.fs.Entry.prototype.wrapEntry = goog.abstractMethod;
  141. /** @override */
  142. goog.testing.fs.Entry.prototype.remove = function() {
  143. var msg = 'removing ' + this.getFullPath();
  144. var self = this;
  145. return this.checkNotDeleted(msg).addCallback(function() {
  146. delete this.parent.children[self.getName()];
  147. self.parent.lastModifiedTimestamp_ = goog.now();
  148. self.deleted = true;
  149. return;
  150. });
  151. };
  152. /** @override */
  153. goog.testing.fs.Entry.prototype.getParent = function() {
  154. var msg = 'getting parent of ' + this.getFullPath();
  155. return this.checkNotDeleted(msg).addCallback(function() {
  156. return this.parent;
  157. });
  158. };
  159. /**
  160. * Return a deferred that will call its errback if this entry has been deleted.
  161. * In addition, the deferred will only run after a timeout of 0, and all its
  162. * callbacks will run with the entry as "this".
  163. *
  164. * @param {string} action The name of the action being performed. For error
  165. * reporting.
  166. * @return {!goog.async.Deferred} The deferred that will be called after a
  167. * timeout of 0.
  168. * @protected
  169. */
  170. goog.testing.fs.Entry.prototype.checkNotDeleted = function(action) {
  171. var d = new goog.async.Deferred(undefined, this);
  172. goog.Timer.callOnce(function() {
  173. if (this.deleted) {
  174. var err = new goog.fs.Error({'name': 'NotFoundError'}, action);
  175. d.errback(err);
  176. } else {
  177. d.callback();
  178. }
  179. }, 0, this);
  180. return d;
  181. };
  182. /**
  183. * A mock directory entry object.
  184. *
  185. * @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
  186. * @param {goog.testing.fs.DirectoryEntry} parent The directory entry directly
  187. * containing this entry. If this is null, that means this is the root
  188. * directory and so is its own parent.
  189. * @param {string} name The name of this entry.
  190. * @param {!Object<!goog.testing.fs.Entry>} children The map of child names to
  191. * entry objects.
  192. * @constructor
  193. * @extends {goog.testing.fs.Entry}
  194. * @implements {goog.fs.DirectoryEntry}
  195. * @final
  196. */
  197. goog.testing.fs.DirectoryEntry = function(fs, parent, name, children) {
  198. goog.testing.fs.DirectoryEntry.base(
  199. this, 'constructor', fs, parent || this, name);
  200. /**
  201. * The map of child names to entry objects.
  202. * @type {!Object<!goog.testing.fs.Entry>}
  203. */
  204. this.children = children;
  205. /**
  206. * The modification time of the directory. Measured using goog.now, which may
  207. * be overridden with mock time providers.
  208. * @type {number}
  209. * @private
  210. */
  211. this.lastModifiedTimestamp_ = goog.now();
  212. };
  213. goog.inherits(goog.testing.fs.DirectoryEntry, goog.testing.fs.Entry);
  214. /**
  215. * Constructs and returns the metadata object for this entry.
  216. * @return {{modificationTime: Date}} The metadata object.
  217. * @private
  218. */
  219. goog.testing.fs.DirectoryEntry.prototype.getMetadata_ = function() {
  220. return {'modificationTime': new Date(this.lastModifiedTimestamp_)};
  221. };
  222. /** @override */
  223. goog.testing.fs.DirectoryEntry.prototype.isFile = function() {
  224. return false;
  225. };
  226. /** @override */
  227. goog.testing.fs.DirectoryEntry.prototype.isDirectory = function() {
  228. return true;
  229. };
  230. /** @override */
  231. goog.testing.fs.DirectoryEntry.prototype.getLastModified = function() {
  232. var msg = 'reading last modified date for ' + this.getFullPath();
  233. return this.checkNotDeleted(msg).addCallback(function() {
  234. return new Date(this.lastModifiedTimestamp_)
  235. });
  236. };
  237. /** @override */
  238. goog.testing.fs.DirectoryEntry.prototype.getMetadata = function() {
  239. var msg = 'reading metadata for ' + this.getFullPath();
  240. return this.checkNotDeleted(msg).addCallback(function() {
  241. return this.getMetadata_()
  242. });
  243. };
  244. /** @override */
  245. goog.testing.fs.DirectoryEntry.prototype.clone = function() {
  246. return new goog.testing.fs.DirectoryEntry(
  247. this.getFileSystem(), this.parent, this.getName(), this.children);
  248. };
  249. /** @override */
  250. goog.testing.fs.DirectoryEntry.prototype.remove = function() {
  251. if (!goog.object.isEmpty(this.children)) {
  252. var d = new goog.async.Deferred();
  253. goog.Timer.callOnce(function() {
  254. d.errback(new goog.fs.Error(
  255. {'name': 'InvalidModificationError'},
  256. 'removing ' + this.getFullPath()));
  257. }, 0, this);
  258. return d;
  259. } else if (this != this.getFileSystem().getRoot()) {
  260. return goog.testing.fs.DirectoryEntry.base(this, 'remove');
  261. } else {
  262. // Root directory, do nothing.
  263. return goog.async.Deferred.succeed();
  264. }
  265. };
  266. /** @override */
  267. goog.testing.fs.DirectoryEntry.prototype.getFile = function(
  268. path, opt_behavior) {
  269. var msg = 'loading file ' + path + ' from ' + this.getFullPath();
  270. opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
  271. return this.checkNotDeleted(msg).addCallback(function() {
  272. try {
  273. return goog.async.Deferred.succeed(this.getFileSync(path, opt_behavior));
  274. } catch (e) {
  275. return goog.async.Deferred.fail(e);
  276. }
  277. });
  278. };
  279. /** @override */
  280. goog.testing.fs.DirectoryEntry.prototype.getDirectory = function(
  281. path, opt_behavior) {
  282. var msg = 'loading directory ' + path + ' from ' + this.getFullPath();
  283. opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
  284. return this.checkNotDeleted(msg).addCallback(function() {
  285. try {
  286. return goog.async.Deferred.succeed(
  287. this.getDirectorySync(path, opt_behavior));
  288. } catch (e) {
  289. return goog.async.Deferred.fail(e);
  290. }
  291. });
  292. };
  293. /**
  294. * Get a file entry synchronously, without waiting for a Deferred to resolve.
  295. *
  296. * @param {string} path The path to the file, relative to this directory.
  297. * @param {goog.fs.DirectoryEntry.Behavior=} opt_behavior The behavior for
  298. * loading the file.
  299. * @param {string=} opt_data The string data encapsulated by the blob.
  300. * @param {string=} opt_type The mime type of the blob.
  301. * @return {!goog.testing.fs.FileEntry} The loaded file.
  302. */
  303. goog.testing.fs.DirectoryEntry.prototype.getFileSync = function(
  304. path, opt_behavior, opt_data, opt_type) {
  305. opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
  306. return (
  307. /** @type {!goog.testing.fs.FileEntry} */ (
  308. this.getEntry_(
  309. path, opt_behavior, true /* isFile */,
  310. goog.bind(function(parent, name) {
  311. return new goog.testing.fs.FileEntry(
  312. this.getFileSystem(), parent, name,
  313. goog.isDef(opt_data) ? opt_data : '', opt_type);
  314. }, this))));
  315. };
  316. /**
  317. * Creates a file synchronously. This is a shorthand for getFileSync, useful for
  318. * setting up tests.
  319. *
  320. * @param {string} path The path to the file, relative to this directory.
  321. * @return {!goog.testing.fs.FileEntry} The created file.
  322. */
  323. goog.testing.fs.DirectoryEntry.prototype.createFileSync = function(path) {
  324. return this.getFileSync(path, goog.fs.DirectoryEntry.Behavior.CREATE);
  325. };
  326. /**
  327. * Get a directory synchronously, without waiting for a Deferred to resolve.
  328. *
  329. * @param {string} path The path to the directory, relative to this one.
  330. * @param {goog.fs.DirectoryEntry.Behavior=} opt_behavior The behavior for
  331. * loading the directory.
  332. * @return {!goog.testing.fs.DirectoryEntry} The loaded directory.
  333. */
  334. goog.testing.fs.DirectoryEntry.prototype.getDirectorySync = function(
  335. path, opt_behavior) {
  336. opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
  337. return (
  338. /** @type {!goog.testing.fs.DirectoryEntry} */ (
  339. this.getEntry_(
  340. path, opt_behavior, false /* isFile */,
  341. goog.bind(function(parent, name) {
  342. return new goog.testing.fs.DirectoryEntry(
  343. this.getFileSystem(), parent, name, {});
  344. }, this))));
  345. };
  346. /**
  347. * Creates a directory synchronously. This is a shorthand for getFileSync,
  348. * useful for setting up tests.
  349. *
  350. * @param {string} path The path to the directory, relative to this directory.
  351. * @return {!goog.testing.fs.DirectoryEntry} The created directory.
  352. */
  353. goog.testing.fs.DirectoryEntry.prototype.createDirectorySync = function(path) {
  354. return this.getDirectorySync(path, goog.fs.DirectoryEntry.Behavior.CREATE);
  355. };
  356. /**
  357. * Get a file or directory entry from a path. This handles parsing the path for
  358. * subdirectories and throwing appropriate errors should something go wrong.
  359. *
  360. * @param {string} path The path to the entry, relative to this directory.
  361. * @param {goog.fs.DirectoryEntry.Behavior} behavior The behavior for loading
  362. * the entry.
  363. * @param {boolean} isFile Whether a file or directory is being loaded.
  364. * @param {function(!goog.testing.fs.DirectoryEntry, string) :
  365. * !goog.testing.fs.Entry} createFn
  366. * The function for creating the entry if it doesn't yet exist. This is
  367. * passed the parent entry and the name of the new entry.
  368. * @return {!goog.testing.fs.Entry} The loaded entry.
  369. * @private
  370. */
  371. goog.testing.fs.DirectoryEntry.prototype.getEntry_ = function(
  372. path, behavior, isFile, createFn) {
  373. // Filter out leading, trailing, and duplicate slashes.
  374. var components = goog.array.filter(path.split('/'), goog.functions.identity);
  375. var basename = /** @type {string} */ (goog.array.peek(components)) || '';
  376. var dir =
  377. goog.string.startsWith(path, '/') ? this.getFileSystem().getRoot() : this;
  378. goog.array.forEach(components.slice(0, -1), function(p) {
  379. var subdir = dir.children[p];
  380. if (!subdir) {
  381. throw new goog.fs.Error(
  382. {'name': 'NotFoundError'},
  383. 'loading ' + path + ' from ' + this.getFullPath() + ' (directory ' +
  384. dir.getFullPath() + '/' + p + ')');
  385. }
  386. dir = subdir;
  387. }, this);
  388. // If there is no basename, the path must resolve to the root directory.
  389. var entry = basename ? dir.children[basename] : dir;
  390. if (!entry) {
  391. if (behavior == goog.fs.DirectoryEntry.Behavior.DEFAULT) {
  392. throw new goog.fs.Error(
  393. {'name': 'NotFoundError'},
  394. 'loading ' + path + ' from ' + this.getFullPath());
  395. } else {
  396. goog.asserts.assert(
  397. behavior == goog.fs.DirectoryEntry.Behavior.CREATE ||
  398. behavior == goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
  399. entry = createFn(dir, basename);
  400. dir.children[basename] = entry;
  401. this.lastModifiedTimestamp_ = goog.now();
  402. return entry;
  403. }
  404. } else if (behavior == goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE) {
  405. throw new goog.fs.Error(
  406. {'name': 'InvalidModificationError'},
  407. 'loading ' + path + ' from ' + this.getFullPath());
  408. } else if (entry.isFile() != isFile) {
  409. throw new goog.fs.Error(
  410. {'name': 'TypeMismatchError'},
  411. 'loading ' + path + ' from ' + this.getFullPath());
  412. } else {
  413. if (behavior == goog.fs.DirectoryEntry.Behavior.CREATE) {
  414. this.lastModifiedTimestamp_ = goog.now();
  415. }
  416. return entry;
  417. }
  418. };
  419. /**
  420. * Returns whether this directory has a child with the given name.
  421. *
  422. * @param {string} name The name of the entry to check for.
  423. * @return {boolean} Whether or not this has a child with the given name.
  424. */
  425. goog.testing.fs.DirectoryEntry.prototype.hasChild = function(name) {
  426. return name in this.children;
  427. };
  428. /** @override */
  429. goog.testing.fs.DirectoryEntry.prototype.removeRecursively = function() {
  430. var msg = 'removing ' + this.getFullPath() + ' recursively';
  431. return this.checkNotDeleted(msg).addCallback(function() {
  432. var d = goog.async.Deferred.succeed(null);
  433. goog.object.forEach(this.children, function(child) {
  434. d.awaitDeferred(
  435. child.isDirectory() ? child.removeRecursively() : child.remove());
  436. });
  437. d.addCallback(function() { return this.remove(); }, this);
  438. return d;
  439. });
  440. };
  441. /** @override */
  442. goog.testing.fs.DirectoryEntry.prototype.listDirectory = function() {
  443. var msg = 'listing ' + this.getFullPath();
  444. return this.checkNotDeleted(msg).addCallback(function() {
  445. return goog.object.getValues(this.children);
  446. });
  447. };
  448. /** @override */
  449. goog.testing.fs.DirectoryEntry.prototype.createPath =
  450. // This isn't really type-safe.
  451. /** @type {!Function} */ (goog.fs.DirectoryEntryImpl.prototype.createPath);
  452. /**
  453. * A mock file entry object.
  454. *
  455. * @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
  456. * @param {!goog.testing.fs.DirectoryEntry} parent The directory entry directly
  457. * containing this entry.
  458. * @param {string} name The name of this entry.
  459. * @param {string} data The data initially contained in the file.
  460. * @param {string=} opt_type The mime type of the blob.
  461. * @constructor
  462. * @extends {goog.testing.fs.Entry}
  463. * @implements {goog.fs.FileEntry}
  464. * @final
  465. */
  466. goog.testing.fs.FileEntry = function(fs, parent, name, data, opt_type) {
  467. goog.testing.fs.FileEntry.base(this, 'constructor', fs, parent, name);
  468. /**
  469. * The internal file blob referenced by this file entry.
  470. * @type {!goog.testing.fs.File}
  471. * @private
  472. */
  473. this.file_ =
  474. new goog.testing.fs.File(name, new Date(goog.now()), data, opt_type);
  475. /**
  476. * The metadata for file.
  477. * @type {{modificationTime: Date}}
  478. * @private
  479. */
  480. this.metadata_ = {'modificationTime': this.file_.lastModifiedDate};
  481. };
  482. goog.inherits(goog.testing.fs.FileEntry, goog.testing.fs.Entry);
  483. /** @override */
  484. goog.testing.fs.FileEntry.prototype.isFile = function() {
  485. return true;
  486. };
  487. /** @override */
  488. goog.testing.fs.FileEntry.prototype.isDirectory = function() {
  489. return false;
  490. };
  491. /** @override */
  492. goog.testing.fs.FileEntry.prototype.clone = function() {
  493. return new goog.testing.fs.FileEntry(
  494. this.getFileSystem(), this.parent, this.getName(),
  495. this.fileSync().toString());
  496. };
  497. /** @override */
  498. goog.testing.fs.FileEntry.prototype.getLastModified = function() {
  499. return this.file().addCallback(function(file) {
  500. return file.lastModifiedDate;
  501. });
  502. };
  503. /** @override */
  504. goog.testing.fs.FileEntry.prototype.getMetadata = function() {
  505. var msg = 'getting metadata for ' + this.getFullPath();
  506. return this.checkNotDeleted(msg).addCallback(function() {
  507. return this.metadata_;
  508. });
  509. };
  510. /** @override */
  511. goog.testing.fs.FileEntry.prototype.createWriter = function() {
  512. var d = new goog.async.Deferred();
  513. goog.Timer.callOnce(
  514. goog.bind(d.callback, d, new goog.testing.fs.FileWriter(this)));
  515. return d;
  516. };
  517. /** @override */
  518. goog.testing.fs.FileEntry.prototype.file = function() {
  519. var msg = 'getting file for ' + this.getFullPath();
  520. return this.checkNotDeleted(msg).addCallback(function() {
  521. return this.fileSync();
  522. });
  523. };
  524. /**
  525. * Get the internal file representation synchronously, without waiting for a
  526. * Deferred to resolve.
  527. *
  528. * @return {!goog.testing.fs.File} The internal file blob referenced by this
  529. * FileEntry.
  530. */
  531. goog.testing.fs.FileEntry.prototype.fileSync = function() {
  532. return this.file_;
  533. };