Skip to content

Commit 461a026

Browse files
committed
Merge branch 'master' into fixAPISampleTests
2 parents 9089519 + 6e570e3 commit 461a026

File tree

7 files changed

+386
-147
lines changed

7 files changed

+386
-147
lines changed

src/harness/vfs.ts

Lines changed: 256 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,24 @@ namespace vfs {
126126
return this._shadowRoot;
127127
}
128128

129+
/**
130+
* Snapshots the current file system, effectively shadowing itself. This is useful for
131+
* generating file system patches using `.diff()` from one snapshot to the next. Performs
132+
* no action if this file system is read-only.
133+
*/
134+
public snapshot() {
135+
if (this.isReadonly) return;
136+
const fs = new FileSystem(this.ignoreCase, { time: this._time });
137+
fs._lazy = this._lazy;
138+
fs._cwd = this._cwd;
139+
fs._time = this._time;
140+
fs._shadowRoot = this._shadowRoot;
141+
fs._dirStack = this._dirStack;
142+
fs.makeReadonly();
143+
this._lazy = {};
144+
this._shadowRoot = fs;
145+
}
146+
129147
/**
130148
* Gets a shadow copy of this file system. Changes to the shadow copy do not affect the
131149
* original, allowing multiple copies of the same core file system without multiple copies
@@ -671,6 +689,160 @@ namespace vfs {
671689
node.ctimeMs = time;
672690
}
673691

692+
/**
693+
* Generates a `FileSet` patch containing all the entries in this `FileSystem` that are not in `base`.
694+
* @param base The base file system. If not provided, this file system's `shadowRoot` is used (if present).
695+
*/
696+
public diff(base = this.shadowRoot) {
697+
const differences: FileSet = {};
698+
const hasDifferences = base ? FileSystem.rootDiff(differences, this, base) : FileSystem.trackCreatedInodes(differences, this, this._getRootLinks());
699+
return hasDifferences ? differences : undefined;
700+
}
701+
702+
/**
703+
* Generates a `FileSet` patch containing all the entries in `chagned` that are not in `base`.
704+
*/
705+
public static diff(changed: FileSystem, base: FileSystem) {
706+
const differences: FileSet = {};
707+
return FileSystem.rootDiff(differences, changed, base) ? differences : undefined;
708+
}
709+
710+
private static diffWorker(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap<string, Inode> | undefined, base: FileSystem, baseLinks: ReadonlyMap<string, Inode> | undefined) {
711+
if (changedLinks && !baseLinks) return FileSystem.trackCreatedInodes(container, changed, changedLinks);
712+
if (baseLinks && !changedLinks) return FileSystem.trackDeletedInodes(container, baseLinks);
713+
if (changedLinks && baseLinks) {
714+
let hasChanges = false;
715+
// track base items missing in changed
716+
baseLinks.forEach((node, basename) => {
717+
if (!changedLinks.has(basename)) {
718+
container[basename] = isDirectory(node) ? new Rmdir() : new Unlink();
719+
hasChanges = true;
720+
}
721+
});
722+
// track changed items missing or differing in base
723+
changedLinks.forEach((changedNode, basename) => {
724+
const baseNode = baseLinks.get(basename);
725+
if (baseNode) {
726+
if (isDirectory(changedNode) && isDirectory(baseNode)) {
727+
return hasChanges = FileSystem.directoryDiff(container, basename, changed, changedNode, base, baseNode) || hasChanges;
728+
}
729+
if (isFile(changedNode) && isFile(baseNode)) {
730+
return hasChanges = FileSystem.fileDiff(container, basename, changed, changedNode, base, baseNode) || hasChanges;
731+
}
732+
if (isSymlink(changedNode) && isSymlink(baseNode)) {
733+
return hasChanges = FileSystem.symlinkDiff(container, basename, changedNode, baseNode) || hasChanges;
734+
}
735+
}
736+
return hasChanges = FileSystem.trackCreatedInode(container, basename, changed, changedNode) || hasChanges;
737+
});
738+
return hasChanges;
739+
}
740+
return false;
741+
}
742+
743+
private static rootDiff(container: FileSet, changed: FileSystem, base: FileSystem) {
744+
while (!changed._lazy.links && changed._shadowRoot) changed = changed._shadowRoot;
745+
while (!base._lazy.links && base._shadowRoot) base = base._shadowRoot;
746+
747+
// no difference if the file systems are the same reference
748+
if (changed === base) return false;
749+
750+
// no difference if the root links are empty and unshadowed
751+
if (!changed._lazy.links && !changed._shadowRoot && !base._lazy.links && !base._shadowRoot) return false;
752+
753+
return FileSystem.diffWorker(container, changed, changed._getRootLinks(), base, base._getRootLinks());
754+
}
755+
756+
private static directoryDiff(container: FileSet, basename: string, changed: FileSystem, changedNode: DirectoryInode, base: FileSystem, baseNode: DirectoryInode) {
757+
while (!changedNode.links && changedNode.shadowRoot) changedNode = changedNode.shadowRoot;
758+
while (!baseNode.links && baseNode.shadowRoot) baseNode = baseNode.shadowRoot;
759+
760+
// no difference if the nodes are the same reference
761+
if (changedNode === baseNode) return false;
762+
763+
// no difference if both nodes are non shadowed and have no entries
764+
if (isEmptyNonShadowedDirectory(changedNode) && isEmptyNonShadowedDirectory(baseNode)) return false;
765+
766+
// no difference if both nodes are unpopulated and point to the same mounted file system
767+
if (!changedNode.links && !baseNode.links &&
768+
changedNode.resolver && changedNode.source !== undefined &&
769+
baseNode.resolver === changedNode.resolver && baseNode.source === changedNode.source) return false;
770+
771+
// no difference if both nodes have identical children
772+
const children: FileSet = {};
773+
if (!FileSystem.diffWorker(children, changed, changed._getLinks(changedNode), base, base._getLinks(baseNode))) {
774+
return false;
775+
}
776+
777+
container[basename] = new Directory(children);
778+
return true;
779+
}
780+
781+
private static fileDiff(container: FileSet, basename: string, changed: FileSystem, changedNode: FileInode, base: FileSystem, baseNode: FileInode) {
782+
while (!changedNode.buffer && changedNode.shadowRoot) changedNode = changedNode.shadowRoot;
783+
while (!baseNode.buffer && baseNode.shadowRoot) baseNode = baseNode.shadowRoot;
784+
785+
// no difference if the nodes are the same reference
786+
if (changedNode === baseNode) return false;
787+
788+
// no difference if both nodes are non shadowed and have no entries
789+
if (isEmptyNonShadowedFile(changedNode) && isEmptyNonShadowedFile(baseNode)) return false;
790+
791+
// no difference if both nodes are unpopulated and point to the same mounted file system
792+
if (!changedNode.buffer && !baseNode.buffer &&
793+
changedNode.resolver && changedNode.source !== undefined &&
794+
baseNode.resolver === changedNode.resolver && baseNode.source === changedNode.source) return false;
795+
796+
const changedBuffer = changed._getBuffer(changedNode);
797+
const baseBuffer = base._getBuffer(baseNode);
798+
799+
// no difference if both buffers are the same reference
800+
if (changedBuffer === baseBuffer) return false;
801+
802+
// no difference if both buffers are identical
803+
if (Buffer.compare(changedBuffer, baseBuffer) === 0) return false;
804+
805+
container[basename] = new File(changedBuffer);
806+
return true;
807+
}
808+
809+
private static symlinkDiff(container: FileSet, basename: string, changedNode: SymlinkInode, baseNode: SymlinkInode) {
810+
// no difference if the nodes are the same reference
811+
if (changedNode.symlink === baseNode.symlink) return false;
812+
container[basename] = new Symlink(changedNode.symlink);
813+
return true;
814+
}
815+
816+
private static trackCreatedInode(container: FileSet, basename: string, changed: FileSystem, node: Inode) {
817+
if (isDirectory(node)) {
818+
const children: FileSet = {};
819+
FileSystem.trackCreatedInodes(children, changed, changed._getLinks(node));
820+
container[basename] = new Directory(children);
821+
}
822+
else if (isSymlink(node)) {
823+
container[basename] = new Symlink(node.symlink);
824+
}
825+
else {
826+
container[basename] = new File(node.buffer || "");
827+
}
828+
return true;
829+
}
830+
831+
private static trackCreatedInodes(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap<string, Inode>) {
832+
// no difference if links are empty
833+
if (!changedLinks.size) return false;
834+
835+
changedLinks.forEach((node, basename) => { FileSystem.trackCreatedInode(container, basename, changed, node); });
836+
return true;
837+
}
838+
839+
private static trackDeletedInodes(container: FileSet, baseLinks: ReadonlyMap<string, Inode>) {
840+
// no difference if links are empty
841+
if (!baseLinks.size) return false;
842+
baseLinks.forEach((node, basename) => { container[basename] = isDirectory(node) ? new Rmdir() : new Unlink(); });
843+
return true;
844+
}
845+
674846
private _mknod(dev: number, type: typeof S_IFREG, mode: number, time?: number): FileInode;
675847
private _mknod(dev: number, type: typeof S_IFDIR, mode: number, time?: number): DirectoryInode;
676848
private _mknod(dev: number, type: typeof S_IFLNK, mode: number, time?: number): SymlinkInode;
@@ -940,10 +1112,10 @@ namespace vfs {
9401112

9411113
private _applyFilesWorker(files: FileSet, dirname: string, deferred: [Symlink | Link | Mount, string][]) {
9421114
for (const key of Object.keys(files)) {
943-
const value = this._normalizeFileSetEntry(files[key]);
1115+
const value = normalizeFileSetEntry(files[key]);
9441116
const path = dirname ? vpath.resolve(dirname, key) : key;
9451117
vpath.validate(path, vpath.ValidationFlags.Absolute);
946-
if (value === null || value === undefined) {
1118+
if (value === null || value === undefined || value instanceof Rmdir || value instanceof Unlink) {
9471119
if (this.stringComparer(vpath.dirname(path), path) === 0) {
9481120
throw new TypeError("Roots cannot be deleted.");
9491121
}
@@ -967,19 +1139,6 @@ namespace vfs {
9671139
}
9681140
}
9691141
}
970-
971-
private _normalizeFileSetEntry(value: FileSet[string]) {
972-
if (value === undefined ||
973-
value === null ||
974-
value instanceof Directory ||
975-
value instanceof File ||
976-
value instanceof Link ||
977-
value instanceof Symlink ||
978-
value instanceof Mount) {
979-
return value;
980-
}
981-
return typeof value === "string" || Buffer.isBuffer(value) ? new File(value) : new Directory(value);
982-
}
9831142
}
9841143

9851144
export interface FileSystemOptions {
@@ -997,12 +1156,9 @@ namespace vfs {
9971156
meta?: Record<string, any>;
9981157
}
9991158

1000-
export interface FileSystemCreateOptions {
1159+
export interface FileSystemCreateOptions extends FileSystemOptions {
10011160
// Sets the documents to add to the file system.
10021161
documents?: ReadonlyArray<documents.TextDocument>;
1003-
1004-
// Sets the initial working directory for the file system.
1005-
cwd?: string;
10061162
}
10071163

10081164
export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants";
@@ -1062,8 +1218,16 @@ namespace vfs {
10621218
*
10631219
* Unless overridden, `/.src` will be the current working directory for the virtual file system.
10641220
*/
1065-
export function createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, cwd }: FileSystemCreateOptions = {}) {
1221+
export function createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, files, cwd, time, meta }: FileSystemCreateOptions = {}) {
10661222
const fs = getBuiltLocal(host, ignoreCase).shadow();
1223+
if (meta) {
1224+
for (const key of Object.keys(meta)) {
1225+
fs.meta.set(key, meta[key]);
1226+
}
1227+
}
1228+
if (time) {
1229+
fs.time(time);
1230+
}
10671231
if (cwd) {
10681232
fs.mkdirpSync(cwd);
10691233
fs.chdir(cwd);
@@ -1083,6 +1247,9 @@ namespace vfs {
10831247
}
10841248
}
10851249
}
1250+
if (files) {
1251+
fs.apply(files);
1252+
}
10861253
return fs;
10871254
}
10881255

@@ -1165,7 +1332,7 @@ namespace vfs {
11651332
* A template used to populate files, directories, links, etc. in a virtual file system.
11661333
*/
11671334
export interface FileSet {
1168-
[name: string]: DirectoryLike | FileLike | Link | Symlink | Mount | null | undefined;
1335+
[name: string]: DirectoryLike | FileLike | Link | Symlink | Mount | Rmdir | Unlink | null | undefined;
11691336
}
11701337

11711338
export type DirectoryLike = FileSet | Directory;
@@ -1201,6 +1368,16 @@ namespace vfs {
12011368
}
12021369
}
12031370

1371+
/** Removes a directory in a `FileSet` */
1372+
export class Rmdir {
1373+
public _rmdirBrand?: never; // brand necessary for proper type guards
1374+
}
1375+
1376+
/** Unlinks a file in a `FileSet` */
1377+
export class Unlink {
1378+
public _unlinkBrand?: never; // brand necessary for proper type guards
1379+
}
1380+
12041381
/** Extended options for a symbolic link in a `FileSet` */
12051382
export class Symlink {
12061383
public readonly symlink: string;
@@ -1273,6 +1450,14 @@ namespace vfs {
12731450
meta?: collections.Metadata;
12741451
}
12751452

1453+
function isEmptyNonShadowedDirectory(node: DirectoryInode) {
1454+
return !node.links && !node.shadowRoot && !node.resolver && !node.source;
1455+
}
1456+
1457+
function isEmptyNonShadowedFile(node: FileInode) {
1458+
return !node.buffer && !node.shadowRoot && !node.resolver && !node.source;
1459+
}
1460+
12761461
function isFile(node: Inode | undefined): node is FileInode {
12771462
return node !== undefined && (node.mode & S_IFMT) === S_IFREG;
12781463
}
@@ -1324,5 +1509,55 @@ namespace vfs {
13241509
}
13251510
return builtLocalCS;
13261511
}
1512+
1513+
function normalizeFileSetEntry(value: FileSet[string]) {
1514+
if (value === undefined ||
1515+
value === null ||
1516+
value instanceof Directory ||
1517+
value instanceof File ||
1518+
value instanceof Link ||
1519+
value instanceof Symlink ||
1520+
value instanceof Mount ||
1521+
value instanceof Rmdir ||
1522+
value instanceof Unlink) {
1523+
return value;
1524+
}
1525+
return typeof value === "string" || Buffer.isBuffer(value) ? new File(value) : new Directory(value);
1526+
}
1527+
1528+
export function formatPatch(patch: FileSet) {
1529+
return formatPatchWorker("", patch);
1530+
}
1531+
1532+
function formatPatchWorker(dirname: string, container: FileSet): string {
1533+
let text = "";
1534+
for (const name of Object.keys(container)) {
1535+
const entry = normalizeFileSetEntry(container[name]);
1536+
const file = dirname ? vpath.combine(dirname, name) : name;
1537+
if (entry === null || entry === undefined || entry instanceof Unlink || entry instanceof Rmdir) {
1538+
text += `//// [${file}] unlink\r\n`;
1539+
}
1540+
else if (entry instanceof Rmdir) {
1541+
text += `//// [${vpath.addTrailingSeparator(file)}] rmdir\r\n`;
1542+
}
1543+
else if (entry instanceof Directory) {
1544+
text += formatPatchWorker(file, entry.files);
1545+
}
1546+
else if (entry instanceof File) {
1547+
const content = typeof entry.data === "string" ? entry.data : entry.data.toString("utf8");
1548+
text += `//// [${file}]\r\n${content}\r\n\r\n`;
1549+
}
1550+
else if (entry instanceof Link) {
1551+
text += `//// [${file}] link(${entry.path})\r\n`;
1552+
}
1553+
else if (entry instanceof Symlink) {
1554+
text += `//// [${file}] symlink(${entry.symlink})\r\n`;
1555+
}
1556+
else if (entry instanceof Mount) {
1557+
text += `//// [${file}] mount(${entry.source})\r\n`;
1558+
}
1559+
}
1560+
return text;
1561+
}
13271562
}
13281563
// tslint:enable:no-null-keyword

0 commit comments

Comments
 (0)