namespace ts.TestFSWithWatch {
export const libFile: File = {
path: "/a/lib/lib.d.ts",
content: `///
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array { length: number; [n: number]: T; }`
};
function getExecutingFilePathFromLibFile(): string {
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
}
export interface TestServerHostCreationParameters {
useCaseSensitiveFileNames?: boolean;
executingFilePath?: string;
currentDirectory?: string;
newLine?: string;
windowsStyleRoot?: string;
environmentVariables?: ESMap;
runWithoutRecursiveWatches?: boolean;
runWithFallbackPolling?: boolean;
inodeWatching?: boolean;
}
export function createWatchedSystem(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
return new TestServerHost(fileOrFolderList, params);
}
export function createServerHost(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
const host = new TestServerHost(fileOrFolderList, params);
// Just like sys, patch the host to use writeFile
patchWriteFileEnsuringDirectory(host);
return host;
}
export interface File {
path: string;
content: string;
fileSize?: number;
}
export interface Folder {
path: string;
}
export interface SymLink {
/** Location of the symlink. */
path: string;
/** Relative path to the real file. */
symLink: string;
}
export type FileOrFolderOrSymLink = File | Folder | SymLink;
export interface FileOrFolderOrSymLinkMap {
[path: string]: string | Omit;
}
export function isFile(fileOrFolderOrSymLink: FileOrFolderOrSymLink): fileOrFolderOrSymLink is File {
return isString((fileOrFolderOrSymLink as File).content);
}
export function isSymLink(fileOrFolderOrSymLink: FileOrFolderOrSymLink): fileOrFolderOrSymLink is SymLink {
return isString((fileOrFolderOrSymLink as SymLink).symLink);
}
interface FSEntryBase {
path: Path;
fullPath: string;
modifiedTime: Date;
}
interface FsFile extends FSEntryBase {
content: string;
fileSize?: number;
}
interface FsFolder extends FSEntryBase {
entries: SortedArray;
}
interface FsSymLink extends FSEntryBase {
symLink: string;
}
export type FSEntry = FsFile | FsFolder | FsSymLink;
function isFsFolder(s: FSEntry | undefined): s is FsFolder {
return !!s && isArray((s as FsFolder).entries);
}
function isFsFile(s: FSEntry | undefined): s is FsFile {
return !!s && isString((s as FsFile).content);
}
function isFsSymLink(s: FSEntry | undefined): s is FsSymLink {
return !!s && isString((s as FsSymLink).symLink);
}
function invokeWatcherCallbacks(callbacks: readonly T[] | undefined, invokeCallback: (cb: T) => void): void {
if (callbacks) {
// The array copy is made to ensure that even if one of the callback removes the callbacks,
// we dont miss any callbacks following it
const cbs = callbacks.slice();
for (const cb of cbs) {
invokeCallback(cb);
}
}
}
function createWatcher(map: MultiMap, path: Path, callback: T): FileWatcher {
map.add(path, callback);
let closed = false;
return {
close: () => {
Debug.assert(!closed);
map.remove(path, callback);
closed = true;
}
};
}
export function getDiffInKeys(map: ESMap, expectedKeys: readonly string[]) {
if (map.size === expectedKeys.length) {
return "";
}
const notInActual: string[] = [];
const duplicates: string[] = [];
const seen = new Map();
forEach(expectedKeys, expectedKey => {
if (seen.has(expectedKey)) {
duplicates.push(expectedKey);
return;
}
seen.set(expectedKey, true);
if (!map.has(expectedKey)) {
notInActual.push(expectedKey);
}
});
const inActualNotExpected: string[] = [];
map.forEach((_value, key) => {
if (!seen.has(key)) {
inActualNotExpected.push(key);
}
seen.set(key, true);
});
return `\n\nNotInActual: ${notInActual}\nDuplicates: ${duplicates}\nInActualButNotInExpected: ${inActualNotExpected}`;
}
export function verifyMapSize(caption: string, map: ESMap, expectedKeys: readonly string[]) {
assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`);
}
export type MapValueTester = [ESMap | undefined, (value: T) => U];
export function checkMap(caption: string, actual: MultiMap, expectedKeys: ReadonlyESMap, valueTester?: MapValueTester): void;
export function checkMap(caption: string, actual: MultiMap, expectedKeys: readonly string[], eachKeyCount: number, valueTester?: MapValueTester): void;
export function checkMap(caption: string, actual: ESMap | MultiMap, expectedKeys: readonly string[], eachKeyCount: undefined): void;
export function checkMap(
caption: string,
actual: ESMap | MultiMap,
expectedKeysMapOrArray: ReadonlyESMap | readonly string[],
eachKeyCountOrValueTester?: number | MapValueTester,
valueTester?: MapValueTester) {
const expectedKeys = isArray(expectedKeysMapOrArray) ? arrayToMap(expectedKeysMapOrArray, s => s, () => eachKeyCountOrValueTester as number) : expectedKeysMapOrArray;
verifyMapSize(caption, actual, isArray(expectedKeysMapOrArray) ? expectedKeysMapOrArray : arrayFrom(expectedKeys.keys()));
if (!isNumber(eachKeyCountOrValueTester)) {
valueTester = eachKeyCountOrValueTester;
}
const [expectedValues, valueMapper] = valueTester || [undefined, undefined!];
expectedKeys.forEach((count, name) => {
assert.isTrue(actual.has(name), `${caption}: expected to contain ${name}, actual keys: ${arrayFrom(actual.keys())}`);
// Check key information only if eachKeyCount is provided
if (!isArray(expectedKeysMapOrArray) || eachKeyCountOrValueTester !== undefined) {
assert.equal((actual as MultiMap).get(name)!.length, count, `${caption}: Expected to be have ${count} entries for ${name}. Actual entry: ${JSON.stringify(actual.get(name))}`);
if (expectedValues) {
assert.deepEqual(
(actual as MultiMap).get(name)!.map(valueMapper),
expectedValues.get(name),
`${caption}:: expected values mismatch for ${name}`
);
}
}
});
}
export function checkArray(caption: string, actual: readonly string[], expected: readonly string[]) {
checkMap(caption, arrayToMap(actual, identity), expected, /*eachKeyCount*/ undefined);
}
export function checkOutputContains(host: TestServerHost, expected: readonly string[]) {
const mapExpected = new Set(expected);
const mapSeen = new Set();
for (const f of host.getOutput()) {
assert.isFalse(mapSeen.has(f), `Already found ${f} in ${JSON.stringify(host.getOutput())}`);
if (mapExpected.has(f)) {
mapExpected.delete(f);
mapSeen.add(f);
}
}
assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(arrayFrom(mapExpected.keys()))} in ${JSON.stringify(host.getOutput())}`);
}
export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | readonly string[]) {
const mapExpectedToBeAbsent = new Set(expectedToBeAbsent);
for (const f of host.getOutput()) {
assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(host.getOutput())}`);
}
}
interface CallbackData {
cb: TimeOutCallback;
args: any[];
ms: number | undefined;
time: number;
}
class Callbacks {
private map: { cb: TimeOutCallback; args: any[]; ms: number | undefined; time: number; }[] = [];
private nextId = 1;
constructor(private host: TestServerHost) {
}
getNextId() {
return this.nextId;
}
register(cb: TimeOutCallback, args: any[], ms?: number) {
const timeoutId = this.nextId;
this.nextId++;
this.map[timeoutId] = { cb, args, ms, time: this.host.getTime() };
return timeoutId;
}
unregister(id: any) {
if (typeof id === "number") {
delete this.map[id];
}
}
count() {
let n = 0;
for (const _ in this.map) {
n++;
}
return n;
}
private invokeCallback({ cb, args, ms, time }: CallbackData) {
if (ms !== undefined) {
const newTime = ms + time;
if (this.host.getTime() < newTime) {
this.host.setTime(newTime);
}
}
cb(...args);
}
invoke(invokeKey?: number) {
if (invokeKey) {
this.invokeCallback(this.map[invokeKey]);
delete this.map[invokeKey];
return;
}
// Note: invoking a callback may result in new callbacks been queued,
// so do not clear the entire callback list regardless. Only remove the
// ones we have invoked.
for (const key in this.map) {
this.invokeCallback(this.map[key]);
delete this.map[key];
}
}
}
type TimeOutCallback = (...args: any[]) => void;
export interface TestFileWatcher {
cb: FileWatcherCallback;
pollingInterval: PollingInterval;
}
export interface TestFsWatcher {
cb: FsWatchCallback;
inode: number | undefined;
}
export interface WatchInvokeOptions {
/** Invokes the directory watcher for the parent instead of the file changed */
invokeDirectoryWatcherInsteadOfFileChanged: boolean;
/** When new file is created, do not invoke watches for it */
ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
/** Invoke the file delete, followed by create instead of file changed */
invokeFileDeleteCreateAsPartInsteadOfChange: boolean;
/** Dont invoke delete watches */
ignoreDelete: boolean;
/** Skip inode check on file or folder create*/
skipInodeCheckOnCreate: boolean;
/** When invoking rename event on fs watch, send event with file name suffixed with tilde */
useTildeAsSuffixInRenameEventFileName: boolean;
}
export enum Tsc_WatchFile {
DynamicPolling = "DynamicPriorityPolling",
}
export enum Tsc_WatchDirectory {
WatchFile = "RecursiveDirectoryUsingFsWatchFile",
NonRecursiveWatchDirectory = "RecursiveDirectoryUsingNonRecursiveWatchDirectory",
DynamicPolling = "RecursiveDirectoryUsingDynamicPriorityPolling"
}
export const timeIncrements = 1000;
export interface TestServerHostOptions {
useCaseSensitiveFileNames: boolean;
executingFilePath: string;
currentDirectory: string;
newLine?: string;
useWindowsStylePaths?: boolean;
environmentVariables?: ESMap;
}
export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost {
args: string[] = [];
private readonly output: string[] = [];
private fs: ESMap = new Map();
private time = timeIncrements;
getCanonicalFileName: (s: string) => string;
private toPath: (f: string) => Path;
private timeoutCallbacks = new Callbacks(this);
private immediateCallbacks = new Callbacks(this);
readonly screenClears: number[] = [];
readonly watchedFiles = createMultiMap();
readonly fsWatches = createMultiMap();
readonly fsWatchesRecursive = createMultiMap();
runWithFallbackPolling: boolean;
public readonly useCaseSensitiveFileNames: boolean;
public readonly newLine: string;
public readonly windowsStyleRoot?: string;
private readonly environmentVariables?: ESMap;
private readonly executingFilePath: string;
private readonly currentDirectory: string;
public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined;
public storeFilesChangingSignatureDuringEmit = true;
watchFile: HostWatchFile;
private inodeWatching: boolean | undefined;
private readonly inodes?: ESMap;
watchDirectory: HostWatchDirectory;
constructor(
fileOrFolderorSymLinkList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[],
{
useCaseSensitiveFileNames, executingFilePath, currentDirectory,
newLine, windowsStyleRoot, environmentVariables,
runWithoutRecursiveWatches, runWithFallbackPolling,
inodeWatching,
}: TestServerHostCreationParameters = {}) {
this.useCaseSensitiveFileNames = !!useCaseSensitiveFileNames;
this.newLine = newLine || "\n";
this.windowsStyleRoot = windowsStyleRoot;
this.environmentVariables = environmentVariables;
currentDirectory = currentDirectory || "/";
this.getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames);
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
this.executingFilePath = this.getHostSpecificPath(executingFilePath || getExecutingFilePathFromLibFile());
this.currentDirectory = this.getHostSpecificPath(currentDirectory);
this.runWithFallbackPolling = !!runWithFallbackPolling;
const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE");
const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY");
if (inodeWatching) {
this.inodeWatching = true;
this.inodes = new Map();
}
const { watchFile, watchDirectory } = createSystemWatchFunctions({
// We dont have polling watch file
// it is essentially fsWatch but lets get that separate from fsWatch and
// into watchedFiles for easier testing
pollingWatchFileWorker: this.watchFileWorker.bind(this),
getModifiedTime: this.getModifiedTime.bind(this),
setTimeout: this.setTimeout.bind(this),
clearTimeout: this.clearTimeout.bind(this),
fsWatchWorker: this.fsWatchWorker.bind(this),
fileSystemEntryExists: this.fileSystemEntryExists.bind(this),
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
getCurrentDirectory: this.getCurrentDirectory.bind(this),
fsSupportsRecursiveFsWatch: tscWatchDirectory ? false : !runWithoutRecursiveWatches,
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
realpath: this.realpath.bind(this),
tscWatchFile,
tscWatchDirectory,
inodeWatching: !!this.inodeWatching,
sysLog: s => this.write(s + this.newLine),
});
this.watchFile = watchFile;
this.watchDirectory = watchDirectory;
this.reloadFS(fileOrFolderorSymLinkList);
}
private nextInode = 0;
private setInode(path: Path) {
if (this.inodes) this.inodes.set(path, this.nextInode++);
}
// Output is pretty
writeOutputIsTTY() {
return true;
}
getNewLine() {
return this.newLine;
}
toNormalizedAbsolutePath(s: string) {
return getNormalizedAbsolutePath(s, this.currentDirectory);
}
toFullPath(s: string) {
return this.toPath(this.toNormalizedAbsolutePath(s));
}
getHostSpecificPath(s: string) {
if (this.windowsStyleRoot && s.startsWith(directorySeparator)) {
return this.windowsStyleRoot + s.substring(1);
}
return s;
}
now() {
this.time += timeIncrements;
return new Date(this.time);
}
getTime() {
return this.time;
}
setTime(time: number) {
this.time = time;
}
private reloadFS(fileOrFolderOrSymLinkList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[]) {
Debug.assert(this.fs.size === 0);
if (isArray(fileOrFolderOrSymLinkList)) {
fileOrFolderOrSymLinkList.forEach(f => this.ensureFileOrFolder(!this.windowsStyleRoot ?
f :
{ ...f, path: this.getHostSpecificPath(f.path) }
));
}
else {
for (const key in fileOrFolderOrSymLinkList) {
if (hasProperty(fileOrFolderOrSymLinkList, key)) {
const path = this.getHostSpecificPath(key);
const value = fileOrFolderOrSymLinkList[key];
if (isString(value)) {
this.ensureFileOrFolder({ path, content: value });
}
else {
this.ensureFileOrFolder({ path, ...value });
}
}
}
}
}
modifyFile(filePath: string, content: string, options?: Partial) {
const path = this.toFullPath(filePath);
const currentEntry = this.fs.get(path);
if (!currentEntry || !isFsFile(currentEntry)) {
throw new Error(`file not present: ${filePath}`);
}
if (options && options.invokeFileDeleteCreateAsPartInsteadOfChange) {
this.removeFileOrFolder(currentEntry, /*isRenaming*/ false, options);
this.ensureFileOrFolder({ path: filePath, content }, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ undefined, /*ignoreParentWatch*/ undefined, options);
}
else {
currentEntry.content = content;
currentEntry.modifiedTime = this.now();
this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now();
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
const directoryFullPath = getDirectoryPath(currentEntry.fullPath);
this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime);
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
}
else {
this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
}
}
}
renameFile(fileName: string, newFileName: string) {
const fullPath = getNormalizedAbsolutePath(fileName, this.currentDirectory);
const path = this.toPath(fullPath);
const file = this.fs.get(path) as FsFile;
Debug.assert(!!file);
// Only remove the file
this.removeFileOrFolder(file, /*isRenaming*/ true);
// Add updated folder with new folder name
const newFullPath = getNormalizedAbsolutePath(newFileName, this.currentDirectory);
const newFile = this.toFsFile({ path: newFullPath, content: file.content });
const newPath = newFile.path;
const basePath = getDirectoryPath(path);
Debug.assert(basePath !== path);
Debug.assert(basePath === getDirectoryPath(newPath));
const baseFolder = this.fs.get(basePath) as FsFolder;
this.addFileOrFolderInFolder(baseFolder, newFile);
}
renameFolder(folderName: string, newFolderName: string) {
const fullPath = getNormalizedAbsolutePath(folderName, this.currentDirectory);
const path = this.toPath(fullPath);
const folder = this.fs.get(path) as FsFolder;
Debug.assert(!!folder);
// Only remove the folder
this.removeFileOrFolder(folder, /*isRenaming*/ true);
// Add updated folder with new folder name
const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory);
const newFolder = this.toFsFolder(newFullPath);
const newPath = newFolder.path;
const basePath = getDirectoryPath(path);
Debug.assert(basePath !== path);
Debug.assert(basePath === getDirectoryPath(newPath));
const baseFolder = this.fs.get(basePath) as FsFolder;
this.addFileOrFolderInFolder(baseFolder, newFolder);
// Invoke watches for files in the folder as deleted (from old path)
this.renameFolderEntries(folder, newFolder);
}
private renameFolderEntries(oldFolder: FsFolder, newFolder: FsFolder) {
for (const entry of oldFolder.entries) {
this.fs.delete(entry.path);
this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Deleted);
entry.fullPath = combinePaths(newFolder.fullPath, getBaseFileName(entry.fullPath));
entry.path = this.toPath(entry.fullPath);
if (newFolder !== oldFolder) {
newFolder.entries.push(entry);
}
this.fs.set(entry.path, entry);
this.setInode(entry.path);
this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created);
if (isFsFolder(entry)) {
this.renameFolderEntries(entry, entry);
}
}
}
ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean, options?: Partial) {
if (isFile(fileOrDirectoryOrSymLink)) {
const file = this.toFsFile(fileOrDirectoryOrSymLink);
// file may already exist when updating existing type declaration file
if (!this.fs.get(file.path)) {
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch, options);
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate, options);
}
}
else if (isSymLink(fileOrDirectoryOrSymLink)) {
const symLink = this.toFsSymLink(fileOrDirectoryOrSymLink);
Debug.assert(!this.fs.get(symLink.path));
const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch, options);
this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate, options);
}
else {
const fullPath = getNormalizedAbsolutePath(fileOrDirectoryOrSymLink.path, this.currentDirectory);
this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch, options);
this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate, options);
}
}
private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined, options: Partial | undefined): FsFolder {
const path = this.toPath(fullPath);
let folder = this.fs.get(path) as FsFolder;
if (!folder) {
folder = this.toFsFolder(fullPath);
const baseFullPath = getDirectoryPath(fullPath);
if (fullPath !== baseFullPath) {
// Add folder in the base folder
const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch, options);
this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch, options);
}
else {
// root folder
Debug.assert(this.fs.size === 0 || !!this.windowsStyleRoot);
this.fs.set(path, folder);
this.setInode(path);
}
}
Debug.assert(isFsFolder(folder));
return folder;
}
private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean, options?: Partial) {
if (!this.fs.has(fileOrDirectory.path)) {
insertSorted(folder.entries, fileOrDirectory, (a, b) => compareStringsCaseSensitive(getBaseFileName(a.path), getBaseFileName(b.path)));
}
folder.modifiedTime = this.now();
this.fs.set(fileOrDirectory.path, fileOrDirectory);
this.setInode(fileOrDirectory.path);
if (ignoreWatch) {
return;
}
const inodeWatching = this.inodeWatching;
if (options?.skipInodeCheckOnCreate) this.inodeWatching = false;
this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
this.inodeWatching = inodeWatching;
}
private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRenaming?: boolean, options?: Partial) {
const basePath = getDirectoryPath(fileOrDirectory.path);
const baseFolder = this.fs.get(basePath) as FsFolder;
if (basePath !== fileOrDirectory.path) {
Debug.assert(!!baseFolder);
baseFolder.modifiedTime = this.now();
filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory);
}
this.fs.delete(fileOrDirectory.path);
if (isFsFolder(fileOrDirectory)) {
Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming);
}
if (!options?.ignoreDelete) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted, /*modifiedTime*/ undefined, options?.useTildeAsSuffixInRenameEventFileName);
this.inodes?.delete(fileOrDirectory.path);
if (!options?.ignoreDelete) this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed, baseFolder.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
}
deleteFile(filePath: string) {
const path = this.toFullPath(filePath);
const currentEntry = this.fs.get(path) as FsFile;
Debug.assert(isFsFile(currentEntry));
this.removeFileOrFolder(currentEntry);
}
deleteFolder(folderPath: string, recursive?: boolean) {
const path = this.toFullPath(folderPath);
const currentEntry = this.fs.get(path) as FsFolder;
Debug.assert(isFsFolder(currentEntry));
if (recursive && currentEntry.entries.length) {
const subEntries = currentEntry.entries.slice();
subEntries.forEach(fsEntry => {
if (isFsFolder(fsEntry)) {
this.deleteFolder(fsEntry.fullPath, recursive);
}
else {
this.removeFileOrFolder(fsEntry);
}
});
}
this.removeFileOrFolder(currentEntry);
}
private watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) {
return createWatcher(
this.watchedFiles,
this.toFullPath(fileName),
{ cb, pollingInterval }
);
}
private fsWatchWorker(
fileOrDirectory: string,
recursive: boolean,
cb: FsWatchCallback,
) {
if (this.runWithFallbackPolling) throw new Error("Need to use fallback polling instead of file system native watching");
const path = this.toFullPath(fileOrDirectory);
// Error if the path does not exist
if (this.inodeWatching && !this.inodes?.has(path)) throw new Error();
const result = createWatcher(
recursive ? this.fsWatchesRecursive : this.fsWatches,
path,
{
cb,
inode: this.inodes?.get(path)
}
) as FsWatchWorkerWatcher;
result.on = noop;
return result;
}
invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, modifiedTime: Date | undefined) {
invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb }) => cb(fileFullPath, eventKind, modifiedTime));
}
private fsWatchCallback(map: MultiMap, fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, entryFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
const path = this.toPath(fullPath);
const currentInode = this.inodes?.get(path);
invokeWatcherCallbacks(map.get(path), ({ cb, inode }) => {
// TODO::
if (this.inodeWatching && inode !== undefined && inode !== currentInode) return;
let relativeFileName = (entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : "");
if (useTildeSuffix) relativeFileName = (relativeFileName ? relativeFileName : getBaseFileName(fullPath)) + "~";
cb(eventName, relativeFileName, modifiedTime);
});
}
invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.fsWatches, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
}
invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.fsWatchesRecursive, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
}
private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) {
return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
}
private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
const basePath = getDirectoryPath(fullPath);
if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) {
this.invokeRecursiveFsWatches(basePath, eventName, modifiedTime, entryFullPath || fullPath, useTildeSuffix);
}
}
invokeFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, useTildeSuffix: boolean | undefined) {
this.invokeFsWatchesCallbacks(fullPath, eventName, modifiedTime, fullPath, useTildeSuffix);
this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, modifiedTime, fullPath, useTildeSuffix);
this.invokeRecursiveFsWatches(fullPath, eventName, modifiedTime, /*entryFullPath*/ undefined, useTildeSuffix);
}
private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind, modifiedTime?: Date, useTildeSuffix?: boolean) {
this.invokeFileWatcher(fileOrFolderFullPath, eventKind, modifiedTime);
this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename", modifiedTime, useTildeSuffix);
}
private toFsEntry(path: string): FSEntryBase {
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
return {
path: this.toPath(fullPath),
fullPath,
modifiedTime: this.now()
};
}
private toFsFile(file: File): FsFile {
const fsFile = this.toFsEntry(file.path) as FsFile;
fsFile.content = file.content;
fsFile.fileSize = file.fileSize;
return fsFile;
}
private toFsSymLink(symLink: SymLink): FsSymLink {
const fsSymLink = this.toFsEntry(symLink.path) as FsSymLink;
fsSymLink.symLink = getNormalizedAbsolutePath(symLink.symLink, getDirectoryPath(fsSymLink.fullPath));
return fsSymLink;
}
private toFsFolder(path: string): FsFolder {
const fsFolder = this.toFsEntry(path) as FsFolder;
fsFolder.entries = [] as FSEntry[] as SortedArray; // https://github.com/Microsoft/TypeScript/issues/19873
return fsFolder;
}
private getRealFsEntry(isFsEntry: (fsEntry: FSEntry) => fsEntry is T, path: Path, fsEntry = this.fs.get(path)!): T | undefined {
if (isFsEntry(fsEntry)) {
return fsEntry;
}
if (isFsSymLink(fsEntry)) {
return this.getRealFsEntry(isFsEntry, this.toPath(fsEntry.symLink));
}
if (fsEntry) {
// This fs entry is something else
return undefined;
}
const realpath = this.toPath(this.realpath(path));
if (path !== realpath) {
return this.getRealFsEntry(isFsEntry, realpath);
}
return undefined;
}
private isFsFile(fsEntry: FSEntry) {
return !!this.getRealFile(fsEntry.path, fsEntry);
}
private getRealFile(path: Path, fsEntry?: FSEntry): FsFile | undefined {
return this.getRealFsEntry(isFsFile, path, fsEntry);
}
private isFsFolder(fsEntry: FSEntry) {
return !!this.getRealFolder(fsEntry.path, fsEntry);
}
private getRealFolder(path: Path, fsEntry = this.fs.get(path)): FsFolder | undefined {
return this.getRealFsEntry(isFsFolder, path, fsEntry);
}
fileSystemEntryExists(s: string, entryKind: FileSystemEntryKind) {
return entryKind === FileSystemEntryKind.File ? this.fileExists(s) : this.directoryExists(s);
}
fileExists(s: string) {
const path = this.toFullPath(s);
return !!this.getRealFile(path);
}
getModifiedTime(s: string) {
const path = this.toFullPath(s);
const fsEntry = this.fs.get(path);
return (fsEntry && fsEntry.modifiedTime)!; // TODO: GH#18217
}
setModifiedTime(s: string, date: Date) {
const path = this.toFullPath(s);
const fsEntry = this.fs.get(path);
if (fsEntry) {
fsEntry.modifiedTime = date;
this.invokeFileAndFsWatches(fsEntry.fullPath, FileWatcherEventKind.Changed, fsEntry.modifiedTime);
}
}
readFile(s: string): string | undefined {
const fsEntry = this.getRealFile(this.toFullPath(s));
return fsEntry ? fsEntry.content : undefined;
}
getFileSize(s: string) {
const path = this.toFullPath(s);
const entry = this.fs.get(path)!;
if (isFsFile(entry)) {
return entry.fileSize ? entry.fileSize : entry.content.length;
}
return undefined!; // TODO: GH#18217
}
directoryExists(s: string) {
const path = this.toFullPath(s);
return !!this.getRealFolder(path);
}
getDirectories(s: string): string[] {
const path = this.toFullPath(s);
const folder = this.getRealFolder(path);
if (folder) {
return mapDefined(folder.entries, entry => this.isFsFolder(entry) ? getBaseFileName(entry.fullPath) : undefined);
}
Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder");
}
getJsDocNodeCheckedConfig(jsDocFileCheckInfo: FileCheckModuleInfo, sourceFilePath: string): JsDocNodeCheckConfig {
Debug.log(jsDocFileCheckInfo.fileNeedCheck.toString());
Debug.log(sourceFilePath);
return {
nodeNeedCheck: false,
checkConfig: [],
};
}
getFileCheckedModuleInfo?(sourceFilePath: string): FileCheckModuleInfo {
Debug.log(sourceFilePath);
return {
fileNeedCheck: false,
checkPayload: undefined,
currentFileName: "",
}
}
getJsDocNodeConditionCheckedResult?(jsDocFileCheckedInfo: FileCheckModuleInfo, jsDocs: JsDocTagInfo[]): ConditionCheckResult {
Debug.log(jsDocFileCheckedInfo.fileNeedCheck.toString());
Debug.log(jsDocs.toString());
return {
valid: true,
message: "",
type: DiagnosticCategory.Warning
};
}
readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
return matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => {
const directories: string[] = [];
const files: string[] = [];
const folder = this.getRealFolder(this.toPath(dir));
if (folder) {
folder.entries.forEach((entry) => {
if (this.isFsFolder(entry)) {
directories.push(getBaseFileName(entry.fullPath));
}
else if (this.isFsFile(entry)) {
files.push(getBaseFileName(entry.fullPath));
}
else {
Debug.fail("Unknown entry");
}
});
}
return { directories, files };
}, path => this.realpath(path));
}
createHash(s: string): string {
return `${generateDjb2Hash(s)}-${s}`;
}
createSHA256Hash(s: string): string {
return sys.createSHA256Hash!(s);
}
// TOOD: record and invoke callbacks to simulate timer events
setTimeout(callback: TimeOutCallback, ms: number, ...args: any[]) {
return this.timeoutCallbacks.register(callback, args, ms);
}
getNextTimeoutId() {
return this.timeoutCallbacks.getNextId();
}
clearTimeout(timeoutId: any): void {
this.timeoutCallbacks.unregister(timeoutId);
}
clearScreen(): void {
this.screenClears.push(this.output.length);
}
checkTimeoutQueueLengthAndRun(expected: number) {
this.checkTimeoutQueueLength(expected);
this.runQueuedTimeoutCallbacks();
}
checkTimeoutQueueLength(expected: number) {
const callbacksCount = this.timeoutCallbacks.count();
assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
}
runQueuedTimeoutCallbacks(timeoutId?: number) {
try {
this.timeoutCallbacks.invoke(timeoutId);
}
catch (e) {
if (e.message === this.exitMessage) {
return;
}
throw e;
}
}
runQueuedImmediateCallbacks(checkCount?: number) {
if (checkCount !== undefined) {
assert.equal(this.immediateCallbacks.count(), checkCount);
}
this.immediateCallbacks.invoke();
}
setImmediate(callback: TimeOutCallback, ...args: any[]) {
return this.immediateCallbacks.register(callback, args);
}
clearImmediate(timeoutId: any): void {
this.immediateCallbacks.unregister(timeoutId);
}
createDirectory(directoryName: string): void {
const folder = this.toFsFolder(directoryName);
// base folder has to be present
const base = getDirectoryPath(folder.path);
const baseFolder = this.fs.get(base) as FsFolder;
Debug.assert(isFsFolder(baseFolder));
Debug.assert(!this.fs.get(folder.path));
this.addFileOrFolderInFolder(baseFolder, folder);
}
writeFile(path: string, content: string): void {
const file = this.toFsFile({ path, content });
// base folder has to be present
const base = getDirectoryPath(file.path);
const folder = Debug.checkDefined(this.getRealFolder(base));
if (folder.path === base) {
if (!this.fs.has(file.path)) {
this.addFileOrFolderInFolder(folder, file);
}
else {
this.modifyFile(path, content);
}
}
else {
this.writeFile(this.realpath(path), content);
}
}
prependFile(path: string, content: string, options?: Partial): void {
this.modifyFile(path, content + this.readFile(path), options);
}
appendFile(path: string, content: string, options?: Partial): void {
this.modifyFile(path, this.readFile(path) + content, options);
}
write(message: string) {
if (Debug.isDebugging) console.log(message);
this.output.push(message);
}
getOutput(): readonly string[] {
return this.output;
}
clearOutput() {
clear(this.output);
this.screenClears.length = 0;
}
serializeOutput(baseline: string[]) {
const output = this.getOutput();
let start = 0;
baseline.push("Output::");
for (const screenClear of this.screenClears) {
baselineOutputs(baseline, output, start, screenClear);
start = screenClear;
baseline.push(">> Screen clear");
}
baselineOutputs(baseline, output, start);
baseline.push("");
this.clearOutput();
}
snap(): ESMap {
const result = new Map();
this.fs.forEach((value, key) => {
const cloneValue = clone(value);
if (isFsFolder(cloneValue)) {
cloneValue.entries = cloneValue.entries.map(clone) as SortedArray;
}
result.set(key, cloneValue);
});
return result;
}
writtenFiles?: ESMap;
diff(baseline: string[], base: ESMap = new Map()) {
this.fs.forEach((newFsEntry, path) => {
diffFsEntry(baseline, base.get(path), newFsEntry, this.inodes?.get(path), this.writtenFiles);
});
base.forEach((oldFsEntry, path) => {
const newFsEntry = this.fs.get(path);
if (!newFsEntry) {
diffFsEntry(baseline, oldFsEntry, newFsEntry, this.inodes?.get(path), this.writtenFiles);
}
});
baseline.push("");
}
serializeWatches(baseline: string[] = []) {
serializeMultiMap(baseline, "PolledWatches", this.watchedFiles);
baseline.push("");
serializeMultiMap(baseline, "FsWatches", this.fsWatches);
baseline.push("");
serializeMultiMap(baseline, "FsWatchesRecursive", this.fsWatchesRecursive);
baseline.push("");
return baseline;
}
realpath(s: string): string {
const fullPath = this.toNormalizedAbsolutePath(s);
const path = this.toPath(fullPath);
if (getDirectoryPath(path) === path) {
// Root
return s;
}
const dirFullPath = this.realpath(getDirectoryPath(fullPath));
const realFullPath = combinePaths(dirFullPath, getBaseFileName(fullPath));
const fsEntry = this.fs.get(this.toPath(realFullPath))!;
if (isFsSymLink(fsEntry)) {
return this.realpath(fsEntry.symLink);
}
// realpath supports non-existent files, so there may not be an fsEntry
return fsEntry?.fullPath || realFullPath;
}
readonly exitMessage = "System Exit";
exitCode: number | undefined;
readonly resolvePath = (s: string) => s;
readonly getExecutingFilePath = () => this.executingFilePath;
readonly getCurrentDirectory = () => this.currentDirectory;
exit(exitCode?: number) {
this.exitCode = exitCode;
throw new Error(this.exitMessage);
}
getEnvironmentVariable(name: string) {
return this.environmentVariables && this.environmentVariables.get(name) || "";
}
}
function diffFsFile(baseline: string[], fsEntry: FsFile, newInode: number | undefined) {
baseline.push(`//// [${fsEntry.fullPath}]${inodeString(newInode)}\r\n${fsEntry.content}`, "");
}
function diffFsSymLink(baseline: string[], fsEntry: FsSymLink, newInode: number | undefined) {
baseline.push(`//// [${fsEntry.fullPath}] symlink(${fsEntry.symLink})${inodeString(newInode)}`);
}
function inodeString(inode: number | undefined) {
return inode !== undefined ? ` Inode:: ${inode}` : "";
}
function diffFsEntry(baseline: string[], oldFsEntry: FSEntry | undefined, newFsEntry: FSEntry | undefined, newInode: number | undefined, writtenFiles: ESMap | undefined): void {
const file = newFsEntry && newFsEntry.fullPath;
if (isFsFile(oldFsEntry)) {
if (isFsFile(newFsEntry)) {
if (oldFsEntry.content !== newFsEntry.content) {
diffFsFile(baseline, newFsEntry, newInode);
}
else if (oldFsEntry.modifiedTime !== newFsEntry.modifiedTime) {
if (oldFsEntry.fullPath !== newFsEntry.fullPath) {
baseline.push(`//// [${file}] file was renamed from file ${oldFsEntry.fullPath}${inodeString(newInode)}`);
}
else if (writtenFiles && !writtenFiles.has(newFsEntry.path)) {
baseline.push(`//// [${file}] file changed its modified time${inodeString(newInode)}`);
}
else {
baseline.push(`//// [${file}] file written with same contents${inodeString(newInode)}`);
}
}
}
else {
baseline.push(`//// [${oldFsEntry.fullPath}] deleted`);
if (isFsSymLink(newFsEntry)) {
diffFsSymLink(baseline, newFsEntry, newInode);
}
}
}
else if (isFsSymLink(oldFsEntry)) {
if (isFsSymLink(newFsEntry)) {
if (oldFsEntry.symLink !== newFsEntry.symLink) {
diffFsSymLink(baseline, newFsEntry, newInode);
}
else if (oldFsEntry.modifiedTime !== newFsEntry.modifiedTime) {
if (oldFsEntry.fullPath !== newFsEntry.fullPath) {
baseline.push(`//// [${file}] symlink was renamed from symlink ${oldFsEntry.fullPath}${inodeString(newInode)}`);
}
else if (writtenFiles && !writtenFiles.has(newFsEntry.path)) {
baseline.push(`//// [${file}] symlink changed its modified time${inodeString(newInode)}`);
}
else {
baseline.push(`//// [${file}] symlink written with same link${inodeString(newInode)}`);
}
}
}
else {
baseline.push(`//// [${oldFsEntry.fullPath}] deleted symlink`);
if (isFsFile(newFsEntry)) {
diffFsFile(baseline, newFsEntry, newInode);
}
}
}
else if (isFsFile(newFsEntry)) {
diffFsFile(baseline, newFsEntry, newInode);
}
else if (isFsSymLink(newFsEntry)) {
diffFsSymLink(baseline, newFsEntry, newInode);
}
}
function serializeMultiMap(baseline: string[], caption: string, multiMap: MultiMap) {
baseline.push(`${caption}::`);
multiMap.forEach((values, key) => {
baseline.push(`${key}:`);
for (const value of values) {
baseline.push(` ${JSON.stringify(value)}`);
}
});
}
function baselineOutputs(baseline: string[], output: readonly string[], start: number, end = output.length) {
let baselinedOutput: string[] | undefined;
for (let i = start; i < end; i++) {
(baselinedOutput ||= []).push(output[i].replace(/Elapsed::\s[0-9]+(?:\.\d+)?ms/g, "Elapsed:: *ms"));
}
if (baselinedOutput) baseline.push(baselinedOutput.join(""));
}
export type TestServerHostTrackingWrittenFiles = TestServerHost & { writtenFiles: ESMap; };
export function changeToHostTrackingWrittenFiles(inputHost: TestServerHost) {
const host = inputHost as TestServerHostTrackingWrittenFiles;
const originalWriteFile = host.writeFile;
host.writtenFiles = new Map();
host.writeFile = (fileName, content) => {
originalWriteFile.call(host, fileName, content);
const path = host.toFullPath(fileName);
host.writtenFiles.set(path, (host.writtenFiles.get(path) || 0) + 1);
};
return host;
}
export const tsbuildProjectsLocation = "/user/username/projects";
export function getTsBuildProjectFilePath(project: string, file: string) {
return `${tsbuildProjectsLocation}/${project}/${file}`;
}
export function getTsBuildProjectFile(project: string, file: string): File {
return {
path: getTsBuildProjectFilePath(project, file),
content: Harness.IO.readFile(`${Harness.IO.getWorkspaceRoot()}/tests/projects/${project}/${file}`)!
};
}
}