622 lines
18 KiB
JavaScript
622 lines
18 KiB
JavaScript
// Copyright 2011 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS-IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview Mock filesystem objects. These are all in the same file to
|
|
* avoid circular dependency issues.
|
|
*
|
|
*/
|
|
|
|
goog.provide('goog.testing.fs.DirectoryEntry');
|
|
goog.provide('goog.testing.fs.Entry');
|
|
goog.provide('goog.testing.fs.FileEntry');
|
|
|
|
goog.require('goog.Timer');
|
|
goog.require('goog.array');
|
|
goog.require('goog.asserts');
|
|
goog.require('goog.async.Deferred');
|
|
goog.require('goog.fs.DirectoryEntry');
|
|
goog.require('goog.fs.DirectoryEntryImpl');
|
|
goog.require('goog.fs.Entry');
|
|
goog.require('goog.fs.Error');
|
|
goog.require('goog.fs.FileEntry');
|
|
goog.require('goog.functions');
|
|
goog.require('goog.object');
|
|
goog.require('goog.string');
|
|
goog.require('goog.testing.fs.File');
|
|
goog.require('goog.testing.fs.FileWriter');
|
|
|
|
|
|
|
|
/**
|
|
* A mock filesystem entry object.
|
|
*
|
|
* @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
|
|
* @param {!goog.testing.fs.DirectoryEntry} parent The directory entry directly
|
|
* containing this entry.
|
|
* @param {string} name The name of this entry.
|
|
* @constructor
|
|
* @implements {goog.fs.Entry}
|
|
*/
|
|
goog.testing.fs.Entry = function(fs, parent, name) {
|
|
/**
|
|
* This entry's filesystem.
|
|
* @type {!goog.testing.fs.FileSystem}
|
|
* @private
|
|
*/
|
|
this.fs_ = fs;
|
|
|
|
/**
|
|
* The name of this entry.
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this.name_ = name;
|
|
|
|
/**
|
|
* The parent of this entry.
|
|
* @type {!goog.testing.fs.DirectoryEntry}
|
|
*/
|
|
this.parent = parent;
|
|
};
|
|
|
|
|
|
/**
|
|
* Whether or not this entry has been deleted.
|
|
* @type {boolean}
|
|
*/
|
|
goog.testing.fs.Entry.prototype.deleted = false;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.isFile = goog.abstractMethod;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.isDirectory = goog.abstractMethod;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.getName = function() {
|
|
return this.name_;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.getFullPath = function() {
|
|
if (this.getName() == '' || this.parent.getName() == '') {
|
|
// The root directory has an empty name
|
|
return '/' + this.name_;
|
|
} else {
|
|
return this.parent.getFullPath() + '/' + this.name_;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!goog.testing.fs.FileSystem}
|
|
* @override
|
|
*/
|
|
goog.testing.fs.Entry.prototype.getFileSystem = function() {
|
|
return this.fs_;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.getLastModified = goog.abstractMethod;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.getMetadata = goog.abstractMethod;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.moveTo = function(parent, opt_newName) {
|
|
var msg = 'moving ' + this.getFullPath() + ' into ' + parent.getFullPath() +
|
|
(opt_newName ? ', renaming to ' + opt_newName : '');
|
|
var newFile;
|
|
return this.checkNotDeleted(msg).
|
|
addCallback(function() { return this.copyTo(parent, opt_newName); }).
|
|
addCallback(function(file) {
|
|
newFile = file;
|
|
return this.remove();
|
|
}).addCallback(function() { return newFile; });
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.copyTo = function(parent, opt_newName) {
|
|
goog.asserts.assert(parent instanceof goog.testing.fs.DirectoryEntry);
|
|
var msg = 'copying ' + this.getFullPath() + ' into ' + parent.getFullPath() +
|
|
(opt_newName ? ', renaming to ' + opt_newName : '');
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
var name = opt_newName || this.getName();
|
|
var entry = this.clone();
|
|
parent.children[name] = entry;
|
|
parent.lastModifiedTimestamp_ = goog.now();
|
|
entry.name_ = name;
|
|
entry.parent = parent;
|
|
return entry;
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!goog.testing.fs.Entry} A shallow copy of this entry object.
|
|
*/
|
|
goog.testing.fs.Entry.prototype.clone = goog.abstractMethod;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.toUrl = function(opt_mimetype) {
|
|
return 'fakefilesystem:' + this.getFullPath();
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.toUri = goog.testing.fs.Entry.prototype.toUrl;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.wrapEntry = goog.abstractMethod;
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.remove = function() {
|
|
var msg = 'removing ' + this.getFullPath();
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
delete this.parent.children[this.getName()];
|
|
this.parent.lastModifiedTimestamp_ = goog.now();
|
|
this.deleted = true;
|
|
return;
|
|
});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.Entry.prototype.getParent = function() {
|
|
var msg = 'getting parent of ' + this.getFullPath();
|
|
return this.checkNotDeleted(msg).
|
|
addCallback(function() { return this.parent; });
|
|
};
|
|
|
|
|
|
/**
|
|
* Return a deferred that will call its errback if this entry has been deleted.
|
|
* In addition, the deferred will only run after a timeout of 0, and all its
|
|
* callbacks will run with the entry as "this".
|
|
*
|
|
* @param {string} action The name of the action being performed. For error
|
|
* reporting.
|
|
* @return {!goog.async.Deferred} The deferred that will be called after a
|
|
* timeout of 0.
|
|
* @protected
|
|
*/
|
|
goog.testing.fs.Entry.prototype.checkNotDeleted = function(action) {
|
|
var d = new goog.async.Deferred(undefined, this);
|
|
goog.Timer.callOnce(function() {
|
|
if (this.deleted) {
|
|
d.errback(new goog.fs.Error(goog.fs.Error.ErrorCode.NOT_FOUND, action));
|
|
} else {
|
|
d.callback();
|
|
}
|
|
}, 0, this);
|
|
return d;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* A mock directory entry object.
|
|
*
|
|
* @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
|
|
* @param {goog.testing.fs.DirectoryEntry} parent The directory entry directly
|
|
* containing this entry. If this is null, that means this is the root
|
|
* directory and so is its own parent.
|
|
* @param {string} name The name of this entry.
|
|
* @param {!Object.<!goog.testing.fs.Entry>} children The map of child names to
|
|
* entry objects.
|
|
* @constructor
|
|
* @extends {goog.testing.fs.Entry}
|
|
* @implements {goog.fs.DirectoryEntry}
|
|
*/
|
|
goog.testing.fs.DirectoryEntry = function(fs, parent, name, children) {
|
|
goog.base(this, fs, parent || this, name);
|
|
|
|
/**
|
|
* The map of child names to entry objects.
|
|
* @type {!Object.<!goog.testing.fs.Entry>}
|
|
*/
|
|
this.children = children;
|
|
|
|
/**
|
|
* The modification time of the directory. Measured using goog.now, which may
|
|
* be overridden with mock time providers.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.lastModifiedTimestamp_ = goog.now();
|
|
};
|
|
goog.inherits(goog.testing.fs.DirectoryEntry, goog.testing.fs.Entry);
|
|
|
|
|
|
/**
|
|
* Constructs and returns the metadata object for this entry.
|
|
* @return {{modificationTime: Date}} The metadata object.
|
|
* @private
|
|
*/
|
|
goog.testing.fs.DirectoryEntry.prototype.getMetadata_ = function() {
|
|
return {
|
|
'modificationTime': new Date(this.lastModifiedTimestamp_)
|
|
};
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.isFile = function() {
|
|
return false;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.isDirectory = function() {
|
|
return true;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.getLastModified = function() {
|
|
var msg = 'reading last modified date for ' + this.getFullPath();
|
|
return this.checkNotDeleted(msg).
|
|
addCallback(function() {return new Date(this.lastModifiedTimestamp_)});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.getMetadata = function() {
|
|
var msg = 'reading metadata for ' + this.getFullPath();
|
|
return this.checkNotDeleted(msg).
|
|
addCallback(function() {return this.getMetadata_()});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.clone = function() {
|
|
return new goog.testing.fs.DirectoryEntry(
|
|
this.getFileSystem(), this.parent, this.getName(), this.children);
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.remove = function() {
|
|
if (!goog.object.isEmpty(this.children)) {
|
|
var d = new goog.async.Deferred();
|
|
goog.Timer.callOnce(function() {
|
|
d.errback(new goog.fs.Error(
|
|
goog.fs.Error.ErrorCode.INVALID_MODIFICATION,
|
|
'removing ' + this.getFullPath()));
|
|
}, 0, this);
|
|
return d;
|
|
} else {
|
|
return goog.base(this, 'remove');
|
|
}
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.getFile = function(
|
|
path, opt_behavior) {
|
|
var msg = 'loading file ' + path + ' from ' + this.getFullPath();
|
|
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
try {
|
|
return goog.async.Deferred.succeed(this.getFileSync(path, opt_behavior));
|
|
} catch (e) {
|
|
return goog.async.Deferred.fail(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.getDirectory = function(
|
|
path, opt_behavior) {
|
|
var msg = 'loading directory ' + path + ' from ' + this.getFullPath();
|
|
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
try {
|
|
return goog.async.Deferred.succeed(
|
|
this.getDirectorySync(path, opt_behavior));
|
|
} catch (e) {
|
|
return goog.async.Deferred.fail(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Get a file entry synchronously, without waiting for a Deferred to resolve.
|
|
*
|
|
* @param {string} path The path to the file, relative to this directory.
|
|
* @param {goog.fs.DirectoryEntry.Behavior=} opt_behavior The behavior for
|
|
* loading the file.
|
|
* @return {!goog.testing.fs.FileEntry} The loaded file.
|
|
*/
|
|
goog.testing.fs.DirectoryEntry.prototype.getFileSync = function(
|
|
path, opt_behavior) {
|
|
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
|
|
return (/** @type {!goog.testing.fs.FileEntry} */ (this.getEntry_(
|
|
path, opt_behavior, true /* isFile */,
|
|
goog.bind(function(parent, name) {
|
|
return new goog.testing.fs.FileEntry(
|
|
this.getFileSystem(), parent, name, '');
|
|
}, this))));
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a file synchronously. This is a shorthand for getFileSync, useful for
|
|
* setting up tests.
|
|
*
|
|
* @param {string} path The path to the file, relative to this directory.
|
|
* @return {!goog.testing.fs.FileEntry} The created file.
|
|
*/
|
|
goog.testing.fs.DirectoryEntry.prototype.createFileSync = function(path) {
|
|
return this.getFileSync(path, goog.fs.DirectoryEntry.Behavior.CREATE);
|
|
};
|
|
|
|
|
|
/**
|
|
* Get a directory synchronously, without waiting for a Deferred to resolve.
|
|
*
|
|
* @param {string} path The path to the directory, relative to this one.
|
|
* @param {goog.fs.DirectoryEntry.Behavior=} opt_behavior The behavior for
|
|
* loading the directory.
|
|
* @return {!goog.testing.fs.DirectoryEntry} The loaded directory.
|
|
*/
|
|
goog.testing.fs.DirectoryEntry.prototype.getDirectorySync = function(
|
|
path, opt_behavior) {
|
|
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
|
|
return (/** @type {!goog.testing.fs.DirectoryEntry} */ (this.getEntry_(
|
|
path, opt_behavior, false /* isFile */,
|
|
goog.bind(function(parent, name) {
|
|
return new goog.testing.fs.DirectoryEntry(
|
|
this.getFileSystem(), parent, name, {});
|
|
}, this))));
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a directory synchronously. This is a shorthand for getFileSync,
|
|
* useful for setting up tests.
|
|
*
|
|
* @param {string} path The path to the directory, relative to this directory.
|
|
* @return {!goog.testing.fs.DirectoryEntry} The created directory.
|
|
*/
|
|
goog.testing.fs.DirectoryEntry.prototype.createDirectorySync = function(path) {
|
|
return this.getDirectorySync(path, goog.fs.DirectoryEntry.Behavior.CREATE);
|
|
};
|
|
|
|
|
|
/**
|
|
* Get a file or directory entry from a path. This handles parsing the path for
|
|
* subdirectories and throwing appropriate errors should something go wrong.
|
|
*
|
|
* @param {string} path The path to the entry, relative to this directory.
|
|
* @param {goog.fs.DirectoryEntry.Behavior} behavior The behavior for loading
|
|
* the entry.
|
|
* @param {boolean} isFile Whether a file or directory is being loaded.
|
|
* @param {function(!goog.testing.fs.DirectoryEntry, string) :
|
|
* !goog.testing.fs.Entry} createFn
|
|
* The function for creating the entry if it doesn't yet exist. This is
|
|
* passed the parent entry and the name of the new entry.
|
|
* @return {!goog.testing.fs.Entry} The loaded entry.
|
|
* @private
|
|
*/
|
|
goog.testing.fs.DirectoryEntry.prototype.getEntry_ = function(
|
|
path, behavior, isFile, createFn) {
|
|
// Filter out leading, trailing, and duplicate slashes.
|
|
var components = goog.array.filter(path.split('/'), goog.functions.identity);
|
|
|
|
var basename = /** @type {string} */ (goog.array.peek(components)) || '';
|
|
var dir = goog.string.startsWith(path, '/') ?
|
|
this.getFileSystem().getRoot() : this;
|
|
|
|
goog.array.forEach(components.slice(0, -1), function(p) {
|
|
var subdir = dir.children[p];
|
|
if (!subdir) {
|
|
throw new goog.fs.Error(
|
|
goog.fs.Error.ErrorCode.NOT_FOUND,
|
|
'loading ' + path + ' from ' + this.getFullPath() + ' (directory ' +
|
|
dir.getFullPath() + '/' + p + ')');
|
|
}
|
|
dir = subdir;
|
|
}, this);
|
|
|
|
// If there is no basename, the path must resolve to the root directory.
|
|
var entry = basename ? dir.children[basename] : dir;
|
|
|
|
if (!entry) {
|
|
if (behavior == goog.fs.DirectoryEntry.Behavior.DEFAULT) {
|
|
throw new goog.fs.Error(
|
|
goog.fs.Error.ErrorCode.NOT_FOUND,
|
|
'loading ' + path + ' from ' + this.getFullPath());
|
|
} else {
|
|
goog.asserts.assert(
|
|
behavior == goog.fs.DirectoryEntry.Behavior.CREATE ||
|
|
behavior == goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
|
|
entry = createFn(dir, basename);
|
|
dir.children[basename] = entry;
|
|
this.lastModifiedTimestamp_ = goog.now();
|
|
return entry;
|
|
}
|
|
} else if (behavior == goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE) {
|
|
throw new goog.fs.Error(
|
|
goog.fs.Error.ErrorCode.PATH_EXISTS,
|
|
'loading ' + path + ' from ' + this.getFullPath());
|
|
} else if (entry.isFile() != isFile) {
|
|
throw new goog.fs.Error(
|
|
goog.fs.Error.ErrorCode.TYPE_MISMATCH,
|
|
'loading ' + path + ' from ' + this.getFullPath());
|
|
} else {
|
|
if (behavior == goog.fs.DirectoryEntry.Behavior.CREATE) {
|
|
this.lastModifiedTimestamp_ = goog.now();
|
|
}
|
|
return entry;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns whether this directory has a child with the given name.
|
|
*
|
|
* @param {string} name The name of the entry to check for.
|
|
* @return {boolean} Whether or not this has a child with the given name.
|
|
*/
|
|
goog.testing.fs.DirectoryEntry.prototype.hasChild = function(name) {
|
|
return name in this.children;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.removeRecursively = function() {
|
|
var msg = 'removing ' + this.getFullPath() + ' recursively';
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
var d = goog.async.Deferred.succeed(null);
|
|
goog.object.forEach(this.children, function(child) {
|
|
d.awaitDeferred(
|
|
child.isDirectory() ? child.removeRecursively() : child.remove());
|
|
});
|
|
d.addCallback(function() { return this.remove(); }, this);
|
|
return d;
|
|
});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.listDirectory = function() {
|
|
var msg = 'listing ' + this.getFullPath();
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
return goog.object.getValues(this.children);
|
|
});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.DirectoryEntry.prototype.createPath =
|
|
// This isn't really type-safe.
|
|
/** @type {!Function} */ (goog.fs.DirectoryEntryImpl.prototype.createPath);
|
|
|
|
|
|
|
|
/**
|
|
* A mock file entry object.
|
|
*
|
|
* @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
|
|
* @param {!goog.testing.fs.DirectoryEntry} parent The directory entry directly
|
|
* containing this entry.
|
|
* @param {string} name The name of this entry.
|
|
* @param {string} data The data initially contained in the file.
|
|
* @constructor
|
|
* @extends {goog.testing.fs.Entry}
|
|
* @implements {goog.fs.FileEntry}
|
|
*/
|
|
goog.testing.fs.FileEntry = function(fs, parent, name, data) {
|
|
goog.base(this, fs, parent, name);
|
|
|
|
/**
|
|
* The internal file blob referenced by this file entry.
|
|
* @type {!goog.testing.fs.File}
|
|
* @private
|
|
*/
|
|
this.file_ = new goog.testing.fs.File(name, new Date(goog.now()), data);
|
|
|
|
/**
|
|
* The metadata for file.
|
|
* @type {{modificationTime: Date}}
|
|
* @private
|
|
*/
|
|
this.metadata_ = {
|
|
'modificationTime': this.file_.lastModifiedDate
|
|
};
|
|
};
|
|
goog.inherits(goog.testing.fs.FileEntry, goog.testing.fs.Entry);
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.FileEntry.prototype.isFile = function() {
|
|
return true;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.FileEntry.prototype.isDirectory = function() {
|
|
return false;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.FileEntry.prototype.clone = function() {
|
|
return new goog.testing.fs.FileEntry(
|
|
this.getFileSystem(), this.parent,
|
|
this.getName(), this.fileSync().toString());
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.FileEntry.prototype.getLastModified = function() {
|
|
return this.file().addCallback(function(file) {
|
|
return file.lastModifiedDate;
|
|
});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.FileEntry.prototype.getMetadata = function() {
|
|
var msg = 'getting metadata for ' + this.getFullPath();
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
return this.metadata_;
|
|
});
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.FileEntry.prototype.createWriter = function() {
|
|
var d = new goog.async.Deferred();
|
|
goog.Timer.callOnce(
|
|
goog.bind(d.callback, d, new goog.testing.fs.FileWriter(this)));
|
|
return d;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.testing.fs.FileEntry.prototype.file = function() {
|
|
var msg = 'getting file for ' + this.getFullPath();
|
|
return this.checkNotDeleted(msg).addCallback(function() {
|
|
return this.fileSync();
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the internal file representation synchronously, without waiting for a
|
|
* Deferred to resolve.
|
|
*
|
|
* @return {!goog.testing.fs.File} The internal file blob referenced by this
|
|
* FileEntry.
|
|
*/
|
|
goog.testing.fs.FileEntry.prototype.fileSync = function() {
|
|
return this.file_;
|
|
};
|