forked from tools/josh
1
0
Fork 0

Test file system and resolve a bunch of bugs

This commit is contained in:
Florine W. Dekker 2019-11-04 19:57:13 +01:00
parent 53ad4f2a49
commit cc1ae80a96
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
7 changed files with 499 additions and 46 deletions

View File

@ -267,6 +267,7 @@ export class Commands {
return node.contents;
})
.filter(it => it !== "")
.join("\n");
}
@ -367,8 +368,8 @@ export class Commands {
return [path, `'${path}' is not a directory.`];
const dirList = [
new Directory({}).nameString("./", path),
new Directory({}).nameString("../", path.parent)
new Directory().nameString("./", path),
new Directory().nameString("../", path.parent)
];
const fileList: string[] = [];
@ -417,6 +418,7 @@ export class Commands {
return error.message;
}
})
.filter(it => it !== "")
.join("\n");
}
@ -479,11 +481,13 @@ export class Commands {
input.hasAnyOption(["f", "force"]),
input.hasAnyOption(["r", "R", "recursive"]),
input.hasOption("no-preserve-root")
)
);
return "";
} catch (error) {
return error.message;
}
})
.filter(it => it !== "")
.join("\n");
}
@ -498,6 +502,7 @@ export class Commands {
return error.message;
}
})
.filter(it => it !== "")
.join("\n");
}
@ -524,6 +529,7 @@ export class Commands {
return error.message;
}
})
.filter(it => it !== "")
.join("\n");
}

View File

@ -1,4 +1,4 @@
import {emptyFunction, getFileExtension, IllegalStateError} from "./Shared";
import {getFileExtension, IllegalArgumentError, IllegalStateError} from "./Shared";
/**
@ -93,7 +93,7 @@ export class FileSystem {
if (this.has(destinationPath))
targetPath = destinationPath.getChild(sourcePath.fileName);
else
targetPath = destinationPath.parent.getChild(destinationPath.fileName);
targetPath = destinationPath;
if (!this.has(targetPath.parent))
throw new Error(`The directory '${targetPath.parent}' does not exist.`);
@ -102,7 +102,7 @@ export class FileSystem {
if (this.has(targetPath))
throw new Error(`The directory or file '${targetPath}' already exists.`);
this.add(targetPath, source, false);
this.add(targetPath, source.copy(), false);
}
/**
@ -116,9 +116,7 @@ export class FileSystem {
return this.root;
const parent = this.get(target.parent);
if (!(parent instanceof Directory))
return undefined;
if (!parent.hasNode(target.fileName))
if (!(parent instanceof Directory) || !parent.hasNode(target.fileName))
return undefined;
return parent.getNode(target.fileName);
@ -131,7 +129,14 @@ export class FileSystem {
* @return `true` if and only if there exists a node at the given path
*/
has(target: Path): boolean {
return target.toString() === "/" || this.has(target.parent);
if (target.toString() === "/")
return true;
const parent = this.get(target.parent);
if (parent === undefined || !(parent instanceof Directory))
return false;
return parent.hasNode(target.fileName);
}
/**
@ -172,12 +177,8 @@ export class FileSystem {
}
const parent = this.get(targetPath.parent);
if (!(parent instanceof Directory)) {
if (force)
return;
else
throw new IllegalStateError(`'${targetPath.parent}' is not a directory, but its child exists.`);
}
if (!(parent instanceof Directory))
throw new IllegalStateError(`'${targetPath.parent}' is not a directory, but its child exists.`);
if (target instanceof Directory) {
if (targetPath.toString() === "/" && !noPreserveRoot)
@ -335,15 +336,6 @@ export abstract class Node {
*/
abstract nameString(name: string, path: Path): string;
/**
* Recursively visits all nodes contained within this node.
*
* @param fun the function to apply to each node, including this node
* @param pre the function to apply to the current node before applying the first `fun`
* @param post the function to apply to the current node after applying the last `fun`
*/
abstract visit(fun: (node: Node) => void, pre: (node: Node) => void, post: (node: Node) => void): void;
/**
* Returns the JSON serialization of this node.
@ -401,7 +393,7 @@ export class Directory extends Node {
constructor(nodes: { [name: string]: Node } = {}) {
super();
this._nodes = Object.assign({}, nodes);
this._nodes = nodes;
}
@ -459,6 +451,9 @@ export class Directory extends Node {
* @param node the node to add to this directory
*/
addNode(name: string, node: Node): void {
if (new Path(`/${name}`).toString() === "/" || name.indexOf("/") >= 0)
throw new IllegalArgumentError(`Cannot add node with name '${name}'.`);
this._nodes[name] = node;
}
@ -497,17 +492,6 @@ export class Directory extends Node {
return `<a href="#" class="dirLink" onclick="execute('cd ${path.toString(true)}');execute('ls')">${name}</a>`;
}
visit(fun: (node: Node) => void,
pre: (node: Node) => void = emptyFunction,
post: (node: Node) => void = emptyFunction) {
pre(this);
fun(this);
Object.keys(this._nodes).forEach(name => this._nodes[name].visit(fun, pre, post));
post(this);
}
/**
* Parses the given object into a directory.
@ -568,14 +552,6 @@ export class File extends Node {
}
}
visit(fun: (node: Node) => void,
pre: (node: Node) => void = emptyFunction,
post: (node: Node) => void = emptyFunction) {
pre(this);
fun(this);
post(this);
}
/**
* Parses the given object into a file.

View File

@ -114,6 +114,22 @@ export function stripHtmlTags(string: string): string {
}
/**
* Indicates that an argument is given to a function that should not have been given.
*
* The user should not be able to reach this state because user input should have been sanitized.
*/
export class IllegalArgumentError extends Error {
/**
* Constructs a new illegal argument error.
*
* @param message a message explaining why the error was thrown
*/
constructor(message: string) {
super(message);
}
}
/**
* Indicates that the program has ended up in a state that it should never end up in.
*

143
src/test/Directory.spec.ts Normal file
View File

@ -0,0 +1,143 @@
import "mocha";
import {expect} from "chai";
import "../main/js/Extensions"
import {Directory, File} from "../main/js/FileSystem";
describe("directory", () => {
let directory: Directory;
beforeEach(() => {
directory = new Directory();
});
describe("constructor", () => {
it("has no nodes by default", () => {
expect(directory.nodeCount).to.equal(0);
});
it("has the given nodes", () => {
expect(new Directory({node: new File()}).hasNode("node")).to.be.true;
});
});
describe("getNode", () => {
it("throws an exception if he node does not exist", () => {
expect(() => directory.getNode("error")).to.throw;
});
it("does not contain itself", () => {
expect(() => directory.getNode(".")).to.throw;
});
it("returns the desired node", () => {
const file = new File();
directory.addNode("file", file);
expect(directory.getNode("file")).to.equal(file);
});
});
describe("hasNode", () => {
it("returns false if the node does not exist", () => {
expect(directory.hasNode("error")).to.be.false;
});
it("returns true if the node exists", () => {
directory.addNode("file", new File());
expect(directory.hasNode("file")).to.be.true;
});
it("returns true if the node is the reflexive node", () => {
expect(directory.hasNode(".")).to.be.true;
});
it("returns true if the node is the parent node", () => {
expect(directory.hasNode("..")).to.be.true;
});
it("returns true if the node refers to itself", () => {
expect(directory.hasNode("")).to.be.true;
});
});
describe("addNode", () => {
it("adds the given node", () => {
directory.addNode("file", new File());
expect(directory.hasNode("file")).to.be.true;
});
it("overwrites an existing node", () => {
directory.addNode("file", new File());
directory.addNode("file", new Directory());
expect(directory.getNode("file")).to.be.instanceOf(Directory);
});
it("refuses to add a node at the reflexive path", () => {
expect(() => directory.addNode(".", new File())).to.throw;
});
it("refuses to add a node at the parent path", () => {
expect(() => directory.addNode("..", new File())).to.throw;
});
it("refuses to add a node that refers to the directory", () => {
expect(() => directory.addNode("", new File())).to.throw;
});
it("refuses to add a node with a name containing a slash", () => {
expect(() => directory.addNode("a/b", new File())).to.throw;
});
});
describe("removeNode", () => {
it("removes the desired node", () => {
directory.addNode("file", new File());
directory.removeNode("file");
expect(directory.nodeCount).to.equal(0);
});
it("empties the directory if the name is empty", () => {
directory.addNode("file", new File());
directory.removeNode("");
expect(directory.nodeCount).to.equal(0);
});
it("empties the directory if the name is reflexive", () => {
directory.addNode("file", new File());
directory.removeNode(".");
expect(directory.nodeCount).to.equal(0);
});
});
describe("copy", () => {
it("returns a deep copy of the directory", () => {
directory = new Directory({
file: new File("contents"),
dir: new Directory()
});
const copy = directory.copy();
(<File>directory.getNode("file")).contents = "changed";
expect((<File>copy.getNode("file")).contents).to.equal("contents");
(<Directory>directory.getNode("dir")).addNode("file2", new File());
expect((<Directory>copy.getNode("dir")).nodeCount).to.equal(0);
directory.removeNode("file");
expect(copy.nodeCount).to.equal(2);
});
});
});

299
src/test/FileSystem.spec.ts Normal file
View File

@ -0,0 +1,299 @@
import "mocha";
import {expect} from "chai";
import "../main/js/Extensions"
import {Directory, File, FileSystem, Path} from "../main/js/FileSystem";
describe("input args", () => {
let fileSystem: FileSystem;
beforeEach(() => {
fileSystem = new FileSystem();
});
describe("constructor", () => {
it("uses the default root if no directory is given", () => {
expect(fileSystem.root).to.not.be.empty;
});
it("uses the given directory as root", () => {
const root = new Directory();
expect(new FileSystem(root).root).to.equal(root);
});
});
describe("add", () => {
it("adds the given node", () => {
fileSystem.add(new Path("/file"), new File(), false);
expect(fileSystem.has(new Path("/file"))).to.be.true;
});
it("adds intermediate directories if the option is given", () => {
fileSystem.add(new Path("/dir1"), new Directory(), false);
fileSystem.add(new Path("/dir1/file1"), new File(), false);
fileSystem.add(new Path("/dir1/dir2/file2"), new File(), true);
expect(fileSystem.get(new Path("/dir1/file1"))).to.be.instanceOf(File);
expect(fileSystem.get(new Path("/dir1/dir2"))).to.be.instanceOf(Directory);
expect(fileSystem.get(new Path("/dir1/dir2/file2"))).to.be.instanceOf(File);
});
it("fails if a node is added as root", () => {
expect(() => fileSystem.add(new Path("/"), new File(), false)).to.throw;
});
it("fails if an intermediate directory does not exist", () => {
expect(() => fileSystem.add(new Path("/dir1/dir2/file2"), new File(), false)).to.throw;
});
it("fails if an intermediate directory is not a directory", () => {
fileSystem.add(new Path("/file1"), new File(), false);
expect(() => fileSystem.add(new Path("/file1/file2"), new File(), false)).to.throw;
});
it("fails if a node already exists at the path", () => {
fileSystem.add(new Path("/file1"), new File(), false);
expect(() => fileSystem.add(new Path("/file1"), new File(), false)).to.throw;
});
});
describe("copy", () => {
it("throws an error if the source does not exist", () => {
expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
});
it("throws an error if the source is a directory and the recursive options it not given", () => {
fileSystem.add(new Path("/src"), new Directory(), false);
expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
});
describe("target is destination", () => {
it("throws an error if the target's parent does not exist", () => {
fileSystem.add(new Path("/src"), new File(), false);
expect(() => fileSystem.copy(new Path("/src"), new Path("/parent/dst"), false)).to.throw;
});
it("throws an error if the target's parent is not a directory", () => {
fileSystem.add(new Path("/src"), new File(), false);
fileSystem.add(new Path("/parent"), new Directory(), false);
expect(() => fileSystem.copy(new Path("/src"), new Path("/parent/dst"), false)).to.throw;
});
it("throws an error if the target already exists", () => {
fileSystem.add(new Path("/src"), new File(), false);
fileSystem.add(new Path("/parent"), new Directory(), false);
fileSystem.add(new Path("/parent/dst"), new File(), false);
expect(() => fileSystem.copy(new Path("/src"), new Path("/parent/dst"), false)).to.throw;
});
it("copies the source file to the target", () => {
fileSystem.add(new Path("/src"), new File(), false);
fileSystem.copy(new Path("/src"), new Path("/dst"), false);
expect(fileSystem.has(new Path("/dst"))).to.be.true;
});
it("copies the source directory to the target", () => {
fileSystem.add(new Path("/src"), new Directory(), false);
fileSystem.add(new Path("/src/file1"), new File(), false);
fileSystem.add(new Path("/src/file2"), new File(), false);
fileSystem.copy(new Path("/src"), new Path("/dst"), true);
expect(fileSystem.has(new Path("/dst"))).to.be.true;
expect(fileSystem.has(new Path("/dst/file1"))).to.be.true;
expect(fileSystem.has(new Path("/dst/file2"))).to.be.true;
});
it("makes a deep copy", () => {
const file = new File("old");
fileSystem.add(new Path("/src"), file, false);
fileSystem.copy(new Path("/src"), new Path("/dst"), false);
file.contents = "new";
expect((<File>fileSystem.get(new Path("/dst"))).contents).to.equal("old");
});
});
describe("target is child of destination", () => {
it("throws an error if the target is not a directory", () => {
fileSystem.add(new Path("/src"), new File(), false);
fileSystem.add(new Path("/dst"), new File(), false);
expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
});
it("throws an error if the target's child already exists", () => {
fileSystem.add(new Path("/src"), new File(), false);
fileSystem.add(new Path("/dst"), new Directory(), false);
fileSystem.add(new Path("/dst/src"), new File(), false);
expect(() => fileSystem.copy(new Path("/src"), new Path("/dst"), false)).to.throw;
});
it("copies the source file to the target", () => {
fileSystem.add(new Path("/src"), new File(), false);
fileSystem.add(new Path("/dst"), new Directory(), false);
fileSystem.copy(new Path("/src"), new Path("/dst"), false);
expect(fileSystem.has(new Path("/dst/src"))).to.be.true;
});
it("copies the source directory to the target", () => {
fileSystem.add(new Path("/src"), new Directory(), false);
fileSystem.add(new Path("/src/file1"), new File(), false);
fileSystem.add(new Path("/src/file2"), new File(), false);
fileSystem.add(new Path("/dst"), new Directory(), false);
fileSystem.copy(new Path("/src"), new Path("/dst"), true);
expect(fileSystem.has(new Path("/dst/src"))).to.be.true;
expect(fileSystem.has(new Path("/dst/src/file1"))).to.be.true;
expect(fileSystem.has(new Path("/dst/src/file2"))).to.be.true;
});
it("makes a deep copy", () => {
const file = new File("old");
fileSystem.add(new Path("/src"), file, false);
fileSystem.add(new Path("/dst"), new Directory(), false);
fileSystem.copy(new Path("/src"), new Path("/dst"), false);
file.contents = "new";
expect((<File>fileSystem.get(new Path("/dst/src"))).contents).to.equal("old");
});
});
});
describe("get", () => {
it("returns the root node for the root path", () => {
expect(fileSystem.get(new Path("/"))).to.equal(fileSystem.root);
});
it("returns undefined if the parent is not a directory", () => {
fileSystem.add(new Path("/file1"), new File(), false);
expect(fileSystem.get(new Path("/file1/file2"))).to.be.undefined;
});
it("returns undefined if the parent does not contain the node", () => {
fileSystem.add(new Path("/dir"), new Directory(), false);
expect(fileSystem.get(new Path("/dir/file"))).to.be.undefined;
});
it("returns the node at the given path", () => {
const file = new File();
fileSystem.add(new Path("/dir"), new Directory(), false);
fileSystem.add(new Path("/dir/file"), file, false);
expect(fileSystem.get(new Path("/dir/file"))).to.equal(file);
});
});
describe("has", () => {
it("returns true for the root path", () => {
expect(fileSystem.has(new Path("/"))).to.be.true;
});
it("returns false if the node does not exist", () => {
expect(fileSystem.has(new Path("/error"))).to.be.false;
});
it("returns true if the node is at the root", () => {
fileSystem.add(new Path("/file"), new File(), false);
expect(fileSystem.has(new Path("/file"))).to.be.true;
});
it("returns false if an intermediate directory does not exist", () => {
fileSystem.add(new Path("/file"), new File(), false);
expect(fileSystem.has(new Path("/dir/file"))).to.be.false;
});
});
describe("move", () => {
it("moves a file", () => {
fileSystem.add(new Path("/src"), new File("old"), false);
fileSystem.move(new Path("/src"), new Path("/dst"));
expect(fileSystem.has(new Path("/src"))).to.be.false;
expect(fileSystem.has(new Path("/dst"))).to.be.true;
});
it("moves a directory", () => {
fileSystem.add(new Path("/src"), new Directory(), false);
fileSystem.add(new Path("/src/file1"), new File(), false);
fileSystem.add(new Path("/src/file2"), new File(), false);
fileSystem.move(new Path("/src"), new Path("/dst"));
expect(fileSystem.has(new Path("/src"))).to.be.false;
expect(fileSystem.has(new Path("/src/file1"))).to.be.false;
expect(fileSystem.has(new Path("/src/file2"))).to.be.false;
expect(fileSystem.has(new Path("/dst"))).to.be.true;
expect(fileSystem.has(new Path("/dst/file1"))).to.be.true;
expect(fileSystem.has(new Path("/dst/file2"))).to.be.true;
});
});
describe("remove", () => {
it("throws an error if the node does not exist", () => {
expect(() => fileSystem.remove(new Path("/error"), false, false, false)).to.throw;
});
it("does nothing if the node does not exist and the force option is given", () => {
expect(() => fileSystem.remove(new Path("/error"), true, false, false)).not.to.throw;
});
it("throws an error if a directory is removed without the recursive option", () => {
fileSystem.add(new Path("/dir"), new Directory(), false);
expect(() => fileSystem.remove(new Path("/dir"), false, false, false)).to.throw;
});
it("throws an error if the root is remove without the recursive or no-preserve-root option", () => {
expect(() => fileSystem.remove(new Path("/"), false, false, false)).to.throw;
expect(() => fileSystem.remove(new Path("/"), false, true, false)).to.throw;
expect(() => fileSystem.remove(new Path("/"), false, false, true)).to.throw;
});
it("removes a file", () => {
fileSystem.add(new Path("/file"), new File(), false);
fileSystem.remove(new Path("/file"), false, false, false);
expect(fileSystem.has(new Path("/file"))).to.be.false;
});
it("removes a directory", () => {
fileSystem.add(new Path("/dir"), new Directory(), false);
fileSystem.remove(new Path("/dir"), false, true, false);
expect(fileSystem.has(new Path("/dir"))).to.be.false;
});
it("removes the root", () => {
fileSystem.add(new Path("/dir"), new Directory(), false);
fileSystem.add(new Path("/file"), new File(), false);
fileSystem.remove(new Path("/"), false, true, true);
expect(fileSystem.has(new Path("/dir"))).to.be.false;
});
});
});

View File

@ -203,7 +203,6 @@ describe("input args", () => {
});
it("should ignore the redirect target if inside quotation marks", () => {
console.log(parser.parse("command '> file'").args);
expect(parser.parse("command '> file'").redirectTarget).to.have.members(["default"]);
});

View File

@ -50,6 +50,20 @@ describe("paths", () => {
expect(new Path("/", "///", "/file").toString()).to.equal("/file");
});
});
describe("interpret", () => {
it("uses only the cwd if no paths are given", () => {
expect(Path.interpret("/cwd").toString()).to.equal("/cwd");
});
it("ignores the cwd if the first path is absolute", () => {
expect(Path.interpret("/cwd", "/dir1", "dir2").toString()).to.equal("/dir1/dir2");
});
it("uses all parts if the first path is relative", () => {
expect(Path.interpret("/cwd", "dir1", "/dir2").toString()).to.equal("/cwd/dir1/dir2");
});
});
});
describe("fileName", () => {