Make sure virtual file system with watch behaves same way as sys/node so we have proper test coverage for symlinks (#57607)

This commit is contained in:
Sheetal Nandi
2024-03-29 09:48:51 -07:00
committed by GitHub
parent 075ebb4928
commit 26358d25a3
65 changed files with 23475 additions and 221 deletions

View File

@@ -398,6 +398,7 @@ class SessionServerHost implements ts.server.ServerHost {
"watchedFiles",
"watchedDirectories",
ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames),
this,
);
constructor(private host: NativeLanguageServiceHost) {

View File

@@ -1,5 +1,4 @@
import {
addRange,
arrayFrom,
compareStringsCaseSensitive,
contains,
@@ -10,6 +9,7 @@ import {
GetCanonicalFileName,
MultiMap,
PollingInterval,
System,
} from "./_namespaces/ts";
export interface TestFileWatcher {
@@ -25,7 +25,7 @@ export interface TestFsWatcher<DirCallback> {
export interface Watches<Data> {
add(path: string, data: Data): void;
remove(path: string, data: Data): void;
forEach(path: string, cb: (data: Data) => void): void;
forEach(path: string, cb: (data: Data, path: string) => void): void;
serialize(baseline: string[]): void;
}
@@ -44,6 +44,7 @@ export function createWatchUtils<PollingWatcherData, FsWatcherData>(
pollingWatchesName: string,
fsWatchesName: string,
getCanonicalFileName: GetCanonicalFileName,
system: Required<Pick<System, "realpath">>,
): WatchUtils<PollingWatcherData, FsWatcherData> {
const pollingWatches = initializeWatches<PollingWatcherData>(pollingWatchesName);
const fsWatches = initializeWatches<FsWatcherData>(fsWatchesName);
@@ -64,6 +65,8 @@ export function createWatchUtils<PollingWatcherData, FsWatcherData>(
const actuals = createMultiMap<string, Data>();
let serialized: Map<string, Data[]> | undefined;
let canonicalPathsToStrings: Map<string, Set<string>> | undefined;
let realToLinked: MultiMap<string, string> | undefined;
let pathToReal: Map<string, string> | undefined;
return {
add,
remove,
@@ -73,40 +76,69 @@ export function createWatchUtils<PollingWatcherData, FsWatcherData>(
function add(path: string, data: Data) {
actuals.add(path, data);
if (actuals.get(path)!.length === 1) {
const canonicalPath = getCanonicalFileName(path);
if (canonicalPath !== path) {
(canonicalPathsToStrings ??= new Map()).set(
canonicalPath,
(canonicalPathsToStrings?.get(canonicalPath) ?? new Set()).add(path),
);
}
if (actuals.get(path)!.length !== 1) return;
const canonicalPath = getCanonicalFileName(path);
if (canonicalPath !== path) {
(canonicalPathsToStrings ??= new Map()).set(
canonicalPath,
(canonicalPathsToStrings?.get(canonicalPath) ?? new Set()).add(path),
);
}
const real = system.realpath(path);
(pathToReal ??= new Map()).set(path, real);
if (real === path) return;
const canonicalReal = getCanonicalFileName(real);
if (getCanonicalFileName(path) !== canonicalReal) {
(realToLinked ??= createMultiMap()).add(canonicalReal, path);
}
}
function remove(path: string, data: Data) {
actuals.remove(path, data);
if (!actuals.has(path)) {
const canonicalPath = getCanonicalFileName(path);
if (canonicalPath !== path) {
const existing = canonicalPathsToStrings!.get(canonicalPath);
if (existing!.size === 1) canonicalPathsToStrings!.delete(canonicalPath);
else existing!.delete(path);
}
if (actuals.has(path)) return;
const canonicalPath = getCanonicalFileName(path);
if (canonicalPath !== path) {
const existing = canonicalPathsToStrings!.get(canonicalPath);
if (existing!.size === 1) canonicalPathsToStrings!.delete(canonicalPath);
else existing!.delete(path);
}
const real = pathToReal?.get(path)!;
pathToReal!.delete(path);
if (real === path) return;
const canonicalReal = getCanonicalFileName(real);
if (getCanonicalFileName(path) !== canonicalReal) {
realToLinked!.remove(canonicalReal, path);
}
}
function forEach(path: string, cb: (data: Data) => void) {
let allData: Data[] | undefined;
allData = addRange(allData, actuals.get(path));
function getAllData(path: string) {
let allData: Map<string, Data[]> | undefined;
addData(path);
const canonicalPath = getCanonicalFileName(path);
if (canonicalPath !== path) allData = addRange(allData, actuals.get(canonicalPath));
if (canonicalPath !== path) addData(canonicalPath);
canonicalPathsToStrings?.get(canonicalPath)?.forEach(canonicalSamePath => {
if (canonicalSamePath !== path && canonicalSamePath !== canonicalPath) {
allData = addRange(allData, actuals.get(canonicalSamePath));
addData(canonicalSamePath);
}
});
allData?.forEach(cb);
return allData;
function addData(path: string) {
const data = actuals.get(path);
if (data) (allData ??= new Map()).set(path, data);
}
}
function forEach(path: string, cb: (data: Data, path: string) => void) {
const real = system.realpath(path);
const canonicalPath = getCanonicalFileName(path);
const canonicalReal = getCanonicalFileName(real);
let allData = canonicalPath === canonicalReal ? getAllData(path) : getAllData(real);
realToLinked?.get(canonicalReal)?.forEach(linked => {
if (allData?.has(linked)) return;
const data = actuals.get(linked);
if (data) (allData ??= new Map()).set(linked, data);
});
allData?.forEach((data, path) => data.forEach(d => cb(d, path)));
}
function serialize(baseline: string[]) {

View File

@@ -65,6 +65,7 @@ import "./unittests/services/preProcessFile";
import "./unittests/services/textChanges";
import "./unittests/services/transpile";
import "./unittests/services/utilities";
import "./unittests/sys/symlinkWatching";
import "./unittests/tsbuild/amdModulesWithOut";
import "./unittests/tsbuild/clean";
import "./unittests/tsbuild/commandLine";

View File

@@ -45,7 +45,6 @@ export interface TscWatchCompileChange<T extends ts.BuilderProgram = ts.EmitAndS
watchOrSolution: WatchOrSolution<T>,
) => void;
// TODO:: sheetal: Needing these fields are technically issues that need to be fixed later
symlinksNotReflected?: readonly string[];
skipStructureCheck?: true;
}
export interface TscWatchCheckOptions {
@@ -220,7 +219,7 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
});
if (edits) {
for (const { caption, edit, timeouts, symlinksNotReflected, skipStructureCheck } of edits) {
for (const { caption, edit, timeouts, skipStructureCheck } of edits) {
applyEdit(sys, baseline, edit, caption);
timeouts(sys, programs, watchOrSolution);
programs = watchBaseline({
@@ -233,7 +232,6 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
caption,
resolutionCache: !skipStructureCheck ? (watchOrSolution as ts.WatchOfConfigFile<T> | undefined)?.getResolutionCache?.() : undefined,
useSourceOfProjectReferenceRedirect,
symlinksNotReflected,
});
}
}
@@ -254,7 +252,6 @@ export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions {
caption?: string;
resolutionCache?: ts.ResolutionCache;
useSourceOfProjectReferenceRedirect?: () => boolean;
symlinksNotReflected?: readonly string[];
}
export function watchBaseline({
baseline,
@@ -266,7 +263,6 @@ export function watchBaseline({
caption,
resolutionCache,
useSourceOfProjectReferenceRedirect,
symlinksNotReflected,
}: WatchBaseline) {
if (baselineSourceMap) generateSourceMapBaselineFiles(sys);
const programs = getPrograms();
@@ -279,7 +275,13 @@ export function watchBaseline({
// Verify program structure and resolution cache when incremental edit with tsc --watch (without build mode)
if (resolutionCache && programs.length) {
ts.Debug.assert(programs.length === 1);
verifyProgramStructureAndResolutionCache(caption!, sys, programs[0][0], resolutionCache, useSourceOfProjectReferenceRedirect, symlinksNotReflected);
verifyProgramStructureAndResolutionCache(
caption!,
sys,
programs[0][0],
resolutionCache,
useSourceOfProjectReferenceRedirect,
);
}
return programs;
}
@@ -289,23 +291,12 @@ function verifyProgramStructureAndResolutionCache(
program: ts.Program,
resolutionCache: ts.ResolutionCache,
useSourceOfProjectReferenceRedirect?: () => boolean,
symlinksNotReflected?: readonly string[],
) {
const options = program.getCompilerOptions();
const compilerHost = ts.createCompilerHostWorker(options, /*setParentNodes*/ undefined, sys);
compilerHost.trace = ts.noop;
compilerHost.writeFile = ts.notImplemented;
compilerHost.useSourceOfProjectReferenceRedirect = useSourceOfProjectReferenceRedirect;
const readFile = compilerHost.readFile;
compilerHost.readFile = fileName => {
const text = readFile.call(compilerHost, fileName);
if (!ts.contains(symlinksNotReflected, fileName)) return text;
// Handle symlinks that dont reflect the watch change
ts.Debug.assert(sys.toPath(sys.realpath(fileName)) !== sys.toPath(fileName));
const file = program.getSourceFile(fileName)!;
ts.Debug.assert(file.text !== text);
return file.text;
};
verifyProgramStructure(
ts.createProgram({
rootNames: program.getRootFileNames(),

View File

@@ -70,6 +70,12 @@ function getExecutingFilePathFromLibFile(): string {
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
}
export const enum TestServerHostOsFlavor {
Windows,
MacOs,
Linux,
}
export interface TestServerHostCreationParameters {
useCaseSensitiveFileNames?: boolean;
executingFilePath?: string;
@@ -81,6 +87,7 @@ export interface TestServerHostCreationParameters {
runWithFallbackPolling?: boolean;
inodeWatching?: boolean;
fsWatchWithTimestamp?: boolean;
osFlavor?: TestServerHostOsFlavor;
}
export function createWatchedSystem(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
@@ -359,6 +366,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
private readonly inodes?: Map<Path, number>;
watchDirectory: HostWatchDirectory;
service?: server.ProjectService;
osFlavor: TestServerHostOsFlavor;
constructor(
fileOrFolderorSymLinkList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[],
{
@@ -372,15 +380,18 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
runWithFallbackPolling,
inodeWatching,
fsWatchWithTimestamp,
osFlavor,
}: TestServerHostCreationParameters = {},
) {
this.useCaseSensitiveFileNames = !!useCaseSensitiveFileNames;
this.newLine = newLine || "\n";
this.osFlavor = osFlavor || TestServerHostOsFlavor.Windows;
if (this.osFlavor === TestServerHostOsFlavor.Linux) runWithoutRecursiveWatches = true;
this.windowsStyleRoot = windowsStyleRoot;
this.environmentVariables = environmentVariables;
currentDirectory = currentDirectory || "/";
this.getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames);
this.watchUtils = createWatchUtils("PolledWatches", "FsWatches", s => this.getCanonicalFileName(s));
this.watchUtils = createWatchUtils("PolledWatches", "FsWatches", s => this.getCanonicalFileName(s), this);
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
this.executingFilePath = this.getHostSpecificPath(executingFilePath || getExecutingFilePathFromLibFile());
this.currentDirectory = this.getHostSpecificPath(currentDirectory);
@@ -508,11 +519,11 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
const directoryFullPath = getDirectoryPath(currentEntry.fullPath);
this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now();
this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, /*modifiedTime*/ undefined);
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", /*modifiedTime*/ undefined, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
this.invokeRecursiveFsWatches(directoryFullPath, "rename", /*modifiedTime*/ undefined, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
}
else {
this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.fullPath, currentEntry.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
}
}
}
@@ -563,7 +574,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
private renameFolderEntries(oldFolder: FsFolder, newFolder: FsFolder) {
for (const entry of oldFolder.entries) {
this.fs.delete(entry.path);
this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Deleted);
this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Deleted, entry.fullPath);
entry.fullPath = combinePaths(newFolder.fullPath, getBaseFileName(entry.fullPath));
entry.path = this.toPath(entry.fullPath);
@@ -572,7 +583,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
}
this.fs.set(entry.path, entry);
this.setInode(entry.path);
this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created);
this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created, entry.fullPath);
if (isFsFolder(entry)) {
this.renameFolderEntries(entry, entry);
}
@@ -636,8 +647,14 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
}
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, folder.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created, fileOrDirectory.fullPath, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
if (this.osFlavor !== TestServerHostOsFlavor.MacOs) {
this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, fileOrDirectory.fullPath, folder.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
const folderOfFolder = getDirectoryPath(folder.fullPath);
if (folderOfFolder !== folder.fullPath) {
this.invokeFsWatches(folderOfFolder, "change", folder.fullPath, options?.useTildeAsSuffixInRenameEventFileName);
}
}
this.inodeWatching = inodeWatching;
}
@@ -654,21 +671,19 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
if (isFsFolder(fileOrDirectory)) {
Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming);
}
if (!options?.ignoreDelete && !options?.ignoreWatches) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted, /*modifiedTime*/ undefined, options?.useTildeAsSuffixInRenameEventFileName);
if (!options?.ignoreDelete && !options?.ignoreWatches) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted, fileOrDirectory.fullPath, /*modifiedTime*/ undefined, options?.useTildeAsSuffixInRenameEventFileName);
this.inodes?.delete(fileOrDirectory.path);
if (!options?.ignoreDelete && !options?.ignoreWatches) 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);
const file = this.getRealFileOrFolder(filePath);
Debug.assert(isFsFile(file));
this.removeFileOrFolder(file);
}
deleteFolder(folderPath: string, recursive?: boolean) {
const path = this.toFullPath(folderPath);
const currentEntry = this.fs.get(path) as FsFolder;
const currentEntry = this.fs.get(path);
Debug.assert(isFsFolder(currentEntry));
if (recursive && currentEntry.entries.length) {
const subEntries = currentEntry.entries.slice();
@@ -691,7 +706,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
);
}
private fsWatchWorker(
fsWatchWorker(
fileOrDirectory: string,
recursive: boolean,
cb: FsWatchCallback,
@@ -713,50 +728,55 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
}
invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, modifiedTime: Date | undefined) {
this.watchUtils.pollingWatches.forEach(fileFullPath, ({ cb }) => cb(fileFullPath, eventKind, modifiedTime));
this.watchUtils.pollingWatches.forEach(fileFullPath, ({ cb }, fullPath) => cb(fullPath, eventKind, modifiedTime));
}
private fsWatchCallback(watches: Watches<TestFsWatcher>, fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, entryFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
private fsWatchCallback(watches: Watches<TestFsWatcher>, fullPath: string, eventName: "rename" | "change", eventFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
const path = this.toPath(fullPath);
const currentInode = this.inodes?.get(path);
watches.forEach(path, ({ cb, inode }) => {
// TODO::
if (this.inodeWatching && inode !== undefined && inode !== currentInode) return;
let relativeFileName = entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : "";
let relativeFileName = eventFullPath ? this.getRelativePathToDirectory(fullPath, eventFullPath) : "";
if (useTildeSuffix) relativeFileName = (relativeFileName ? relativeFileName : getBaseFileName(fullPath)) + "~";
cb(eventName, relativeFileName, modifiedTime);
cb(eventName, relativeFileName);
});
}
invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.watchUtils.fsWatches, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", eventFullPath: string | undefined, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.watchUtils.fsWatches, fullPath, eventName, eventFullPath, useTildeSuffix);
}
invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.watchUtils.fsWatchesRecursive, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", eventFullPath: string | undefined, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.watchUtils.fsWatchesRecursive, fullPath, eventName, eventFullPath, 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);
private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", eventFullPath: string | undefined, useTildeSuffix?: boolean) {
this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, eventFullPath, useTildeSuffix);
const basePath = getDirectoryPath(fullPath);
if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) {
this.invokeRecursiveFsWatches(basePath, eventName, /*modifiedTime*/ undefined, entryFullPath || fullPath, useTildeSuffix);
this.invokeRecursiveFsWatches(basePath, eventName, eventFullPath, 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*/ undefined, fullPath, useTildeSuffix);
this.invokeRecursiveFsWatches(fullPath, eventName, modifiedTime, /*entryFullPath*/ undefined, useTildeSuffix);
invokeFsWatches(fullPath: string, eventName: "rename" | "change", eventFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
this.invokeFsWatchesCallbacks(fullPath, eventName, eventFullPath, useTildeSuffix);
this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, eventFullPath, useTildeSuffix);
this.invokeRecursiveFsWatches(fullPath, eventName, eventFullPath, useTildeSuffix);
}
private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind, modifiedTime?: Date, useTildeSuffix?: boolean) {
private invokeFileAndFsWatches(
fileOrFolderFullPath: string,
eventKind: FileWatcherEventKind,
eventFullPath: string | undefined,
modifiedTime?: Date,
useTildeSuffix?: boolean,
) {
this.invokeFileWatcher(fileOrFolderFullPath, eventKind, modifiedTime);
this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename", modifiedTime, useTildeSuffix);
this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename", eventFullPath, useTildeSuffix);
}
private toFsEntry(path: string): FSEntryBase {
@@ -825,6 +845,10 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
return this.getRealFsEntry(isFsFolder, path, fsEntry);
}
private getRealFileOrFolder(s: string): FsFile | FsFolder | undefined {
return this.getRealFsEntry((entry): entry is FsFile | FsFolder => !!entry && !isFsSymLink(entry), this.toFullPath(s));
}
fileSystemEntryExists(s: string, entryKind: FileSystemEntryKind) {
return entryKind === FileSystemEntryKind.File ? this.fileExists(s) : this.directoryExists(s);
}
@@ -835,17 +859,14 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
}
getModifiedTime(s: string) {
const path = this.toFullPath(s);
const fsEntry = this.fs.get(path);
return (fsEntry && fsEntry.modifiedTime)!; // TODO: GH#18217
return this.getRealFileOrFolder(s)?.modifiedTime;
}
setModifiedTime(s: string, date: Date) {
const path = this.toFullPath(s);
const fsEntry = this.fs.get(path);
const fsEntry = this.getRealFileOrFolder(s);
if (fsEntry) {
fsEntry.modifiedTime = date;
this.invokeFileAndFsWatches(fsEntry.fullPath, FileWatcherEventKind.Changed, fsEntry.modifiedTime);
this.invokeFileAndFsWatches(fsEntry.fullPath, FileWatcherEventKind.Changed, fsEntry.fullPath, fsEntry.modifiedTime);
}
}

View File

@@ -0,0 +1,788 @@
import * as fs from "fs";
import {
IO,
} from "../../_namespaces/Harness";
import * as ts from "../../_namespaces/ts";
import {
defer,
Deferred,
} from "../../_namespaces/Utils";
import {
createWatchedSystem,
FileOrFolderOrSymLinkMap,
TestServerHostOsFlavor,
} from "../helpers/virtualFileSystemWithWatch";
describe("unittests:: sys:: symlinkWatching::", () => {
function delayedOp(op: () => void, delay: number) {
ts.sys.setTimeout!(op, delay);
}
function modifiedTimeToString(d: Date | undefined) {
if (!d) return undefined;
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${d.getMilliseconds().toString().padStart(3, "0")}`;
}
function verifyWatchFile(
scenario: string,
sys: ts.System,
file: string,
link: string,
watchOptions: Pick<ts.WatchOptions, "watchFile">,
getFileName?: (file: string) => string,
) {
it(scenario, async () => {
const fileResult = watchFile(file);
const linkResult = watchFile(link);
await writeFile(file);
await writeFile(link);
fileResult.watcher.close();
linkResult.watcher.close();
function watchFile(toWatch: string) {
const result = {
watcher: sys.watchFile!(
toWatch,
(fileName, eventKind, modifiedTime) => {
assert.equal(fileName, toWatch);
assert.equal(eventKind, ts.FileWatcherEventKind.Changed);
const actual = modifiedTimeToString(modifiedTime);
assert(actual === undefined || actual === modifiedTimeToString(sys.getModifiedTime!(file)));
result.deferred.resolve();
},
10,
watchOptions,
),
deferred: undefined! as Deferred<void>,
};
return result;
}
async function writeFile(onFile: string) {
fileResult.deferred = defer();
linkResult.deferred = defer();
delayedOp(() => sys.writeFile(getFileName?.(onFile) ?? onFile, "export const x = 100;"), 100);
// Should invoke on file as well as link
await fileResult.deferred.promise;
await linkResult.deferred.promise;
}
});
}
interface EventAndFileName {
event: string;
fileName: string | null | undefined;
}
interface ExpectedEventAndFileName {
event: string | readonly string[]; // Its expected event name or any of the event names
fileName: string | null | undefined;
}
type FsWatch<System extends ts.System> = (dir: string, recursive: boolean, cb: ts.FsWatchCallback, sys: System) => ts.FileWatcher;
interface WatchDirectoryResult {
dir: string;
watcher: ts.FileWatcher;
actual: EventAndFileName[];
}
function watchDirectory<System extends ts.System>(
sys: System,
fsWatch: FsWatch<System>,
dir: string,
recursive: boolean,
) {
const result: WatchDirectoryResult = {
dir,
watcher: fsWatch(
dir,
recursive,
(event, fileName) => result.actual.push({ event, fileName: fileName ? ts.normalizeSlashes(fileName) : fileName }),
sys,
),
actual: [],
};
return result;
}
function initializeWatchDirectoryResult(...results: WatchDirectoryResult[]) {
results.forEach(result => result.actual.length = 0);
}
function verfiyWatchDirectoryResult(
opType: string,
dirResult: WatchDirectoryResult,
linkResult: WatchDirectoryResult,
expectedResult: readonly ExpectedEventAndFileName[] | undefined,
) {
const deferred = defer();
delayedOp(() => {
if (opType !== "init") {
verifyEventAndFileNames(`${opType}:: dir`, dirResult.actual, expectedResult);
verifyEventAndFileNames(`${opType}:: link`, linkResult.actual, expectedResult);
}
deferred.resolve();
}, 4000);
return deferred.promise;
}
function verifyEventAndFileNames(
prefix: string,
actual: readonly EventAndFileName[],
expected: readonly ExpectedEventAndFileName[] | undefined,
) {
assert(actual.length >= (expected?.length ?? 0), `${prefix}:: Expected ${JSON.stringify(expected)} events, got ${JSON.stringify(actual)}`);
let expectedIndex = 0;
for (const a of actual) {
if (isExpectedEventAndFileName(a, expected![expectedIndex])) {
expectedIndex++;
continue;
}
// Previous event repeated?
if (isExpectedEventAndFileName(a, expected![expectedIndex - 1])) continue;
ts.Debug.fail(`${prefix}:: Expected ${JSON.stringify(expected)} events, got ${JSON.stringify(actual)}`);
}
assert(expectedIndex >= (expected?.length ?? 0), `${prefix}:: Should get all events: Expected ${JSON.stringify(expected)} events, got ${JSON.stringify(actual)}`);
}
function isExpectedEventAndFileName(actual: EventAndFileName, expected: ExpectedEventAndFileName | undefined) {
return !!expected &&
actual.fileName === expected.fileName &&
(ts.isString(expected.event) ? actual.event === expected.event : ts.contains(expected.event, actual.event));
}
interface FsEventsForWatchDirectory extends Record<string, readonly ExpectedEventAndFileName[] | undefined> {
// The first time events are most of the time are not predictable, so just create random file for that reason
init?: readonly ExpectedEventAndFileName[];
fileCreate: readonly ExpectedEventAndFileName[];
linkFileCreate: readonly ExpectedEventAndFileName[];
fileChange: readonly ExpectedEventAndFileName[];
fileModifiedTimeChange: readonly ExpectedEventAndFileName[];
linkModifiedTimeChange: readonly ExpectedEventAndFileName[];
linkFileChange: readonly ExpectedEventAndFileName[];
fileDelete: readonly ExpectedEventAndFileName[];
linkFileDelete: readonly ExpectedEventAndFileName[];
}
function verifyWatchDirectoryUsingFsEvents<System extends ts.System>(
sys: System,
fsWatch: FsWatch<System>,
dir: string,
link: string,
osFlavor: TestServerHostOsFlavor,
) {
it(`watchDirectory using fsEvents`, async () => {
const tableOfEvents: FsEventsForWatchDirectory = osFlavor === TestServerHostOsFlavor.MacOs ?
{
fileCreate: [
{ event: "rename", fileName: "file1.ts" },
],
linkFileCreate: [
{ event: "rename", fileName: "file2.ts" },
],
fileChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "file1.ts" },
],
linkFileChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "file2.ts" },
],
fileModifiedTimeChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "file1.ts" },
],
linkModifiedTimeChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "file2.ts" },
],
fileDelete: [
{ event: "rename", fileName: "file1.ts" },
],
linkFileDelete: [
{ event: "rename", fileName: "file2.ts" },
],
} :
osFlavor === TestServerHostOsFlavor.Windows ?
{
fileCreate: [
{ event: "rename", fileName: "file1.ts" },
{ event: "change", fileName: "file1.ts" },
],
linkFileCreate: [
{ event: "rename", fileName: "file2.ts" },
{ event: "change", fileName: "file2.ts" },
],
fileChange: [
{ event: "change", fileName: "file1.ts" },
],
linkFileChange: [
{ event: "change", fileName: "file2.ts" },
],
fileModifiedTimeChange: [
{ event: "change", fileName: "file1.ts" },
],
linkModifiedTimeChange: [
{ event: "change", fileName: "file2.ts" },
],
fileDelete: [
{ event: "rename", fileName: "file1.ts" },
],
linkFileDelete: [
{ event: "rename", fileName: "file2.ts" },
],
} :
{
fileCreate: [
{ event: "rename", fileName: "file1.ts" },
{ event: "change", fileName: "file1.ts" },
],
linkFileCreate: [
{ event: "rename", fileName: "file2.ts" },
{ event: "change", fileName: "file2.ts" },
],
fileChange: [
{ event: "change", fileName: "file1.ts" },
],
linkFileChange: [
{ event: "change", fileName: "file2.ts" },
],
fileModifiedTimeChange: [
{ event: "change", fileName: "file1.ts" },
],
linkModifiedTimeChange: [
{ event: "change", fileName: "file2.ts" },
],
fileDelete: [
{ event: "rename", fileName: "file1.ts" },
],
linkFileDelete: [
{ event: "rename", fileName: "file2.ts" },
],
};
await testWatchDirectoryOperations(
sys,
fsWatch,
tableOfEvents,
operation,
dir,
link,
/*recursive*/ false,
[
"init",
"fileCreate",
"linkFileCreate",
"fileChange",
"linkFileChange",
"fileModifiedTimeChange",
"linkModifiedTimeChange",
"fileDelete",
"linkFileDelete",
],
);
function operation(opType: keyof FsEventsForWatchDirectory) {
switch (opType) {
case "init":
sys.writeFile(`${dir}/init.ts`, "export const x = 100;");
break;
case "fileCreate":
case "linkFileCreate":
sys.writeFile(fileName(opType), "export const x = 100;");
break;
case "fileChange":
case "linkFileChange":
sys.writeFile(fileName(opType), "export const x2 = 100;");
break;
case "fileModifiedTimeChange":
case "linkModifiedTimeChange":
sys.setModifiedTime!(fileName(opType), new Date());
break;
case "fileDelete":
case "linkFileDelete":
sys.deleteFile!(fileName(opType));
break;
}
}
function fileName(opType: string) {
return ts.startsWith(opType, "file") ?
`${dir}/file1.ts` :
`${link}/file2.ts`;
}
});
}
interface RecursiveFsEventsForWatchDirectory extends FsEventsForWatchDirectory {
linkSubFileCreate: readonly ExpectedEventAndFileName[];
linkSubFileChange: readonly ExpectedEventAndFileName[];
linkSubModifiedTimeChange: readonly ExpectedEventAndFileName[];
linkSubFileDelete: readonly ExpectedEventAndFileName[] | undefined;
parallelFileCreate: readonly ExpectedEventAndFileName[] | undefined;
parallelLinkFileCreate: readonly ExpectedEventAndFileName[] | undefined;
parallelFileChange: readonly ExpectedEventAndFileName[] | undefined;
parallelLinkFileChange: readonly ExpectedEventAndFileName[] | undefined;
parallelFileModifiedTimeChange: readonly ExpectedEventAndFileName[] | undefined;
parallelLinkModifiedTimeChange: readonly ExpectedEventAndFileName[] | undefined;
parallelFileDelete: readonly ExpectedEventAndFileName[] | undefined;
parallelLinkFileDelete: readonly ExpectedEventAndFileName[] | undefined;
}
function verifyRecursiveWatchDirectoryUsingFsEvents<System extends ts.System>(
sys: System,
fsWatch: FsWatch<System>,
dir: string,
link: string,
osFlavor: TestServerHostOsFlavor.Windows | TestServerHostOsFlavor.MacOs,
) {
const tableOfEvents: RecursiveFsEventsForWatchDirectory = osFlavor === TestServerHostOsFlavor.MacOs ?
{
fileCreate: [
{ event: "rename", fileName: "sub/folder/file1.ts" },
],
linkFileCreate: [
{ event: "rename", fileName: "sub/folder/file2.ts" },
],
fileChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "sub/folder/file1.ts" },
],
linkFileChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "sub/folder/file2.ts" },
],
fileModifiedTimeChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "sub/folder/file1.ts" },
],
linkModifiedTimeChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "sub/folder/file2.ts" },
],
fileDelete: [
{ event: "rename", fileName: "sub/folder/file1.ts" },
],
linkFileDelete: [
{ event: "rename", fileName: "sub/folder/file2.ts" },
],
linkSubFileCreate: [
{ event: "rename", fileName: "sub/folder/file3.ts" },
],
linkSubFileChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "sub/folder/file3.ts" },
],
linkSubModifiedTimeChange: [
// On MacOs 18 and below we might get rename or change and its not deterministic
{ event: ["rename", "change"], fileName: "sub/folder/file3.ts" },
],
linkSubFileDelete: [
{ event: "rename", fileName: "sub/folder/file3.ts" },
],
parallelFileCreate: undefined,
parallelLinkFileCreate: undefined,
parallelFileChange: undefined,
parallelLinkFileChange: undefined,
parallelFileModifiedTimeChange: undefined,
parallelLinkModifiedTimeChange: undefined,
parallelFileDelete: undefined,
parallelLinkFileDelete: undefined,
} :
{
fileCreate: [
{ event: "rename", fileName: "sub/folder/file1.ts" },
{ event: "change", fileName: "sub/folder/file1.ts" },
{ event: "change", fileName: "sub/folder" },
],
linkFileCreate: [
{ event: "rename", fileName: "sub/folder/file2.ts" },
{ event: "change", fileName: "sub/folder/file2.ts" },
{ event: "change", fileName: "sub/folder" },
],
fileChange: [
{ event: "change", fileName: "sub/folder/file1.ts" },
],
linkFileChange: [
{ event: "change", fileName: "sub/folder/file2.ts" },
],
fileModifiedTimeChange: [
{ event: "change", fileName: "sub/folder/file1.ts" },
],
linkModifiedTimeChange: [
{ event: "change", fileName: "sub/folder/file2.ts" },
],
fileDelete: [
{ event: "rename", fileName: "sub/folder/file1.ts" },
],
linkFileDelete: [
{ event: "rename", fileName: "sub/folder/file2.ts" },
],
linkSubFileCreate: [
{ event: "rename", fileName: "sub/folder/file3.ts" },
{ event: "change", fileName: "sub/folder/file3.ts" },
{ event: "change", fileName: "sub/folder" },
],
linkSubFileChange: [
{ event: "change", fileName: "sub/folder/file3.ts" },
],
linkSubModifiedTimeChange: [
{ event: "change", fileName: "sub/folder/file3.ts" },
],
linkSubFileDelete: [
{ event: "rename", fileName: "sub/folder/file3.ts" },
],
parallelFileCreate: undefined,
parallelLinkFileCreate: undefined,
parallelFileChange: undefined,
parallelLinkFileChange: undefined,
parallelFileModifiedTimeChange: undefined,
parallelLinkModifiedTimeChange: undefined,
parallelFileDelete: undefined,
parallelLinkFileDelete: undefined,
};
it(`recursive watchDirectory using fsEvents`, async () => {
await testWatchDirectoryOperations(
sys,
fsWatch,
tableOfEvents,
watchDirectoryOperation,
dir,
link,
/*recursive*/ true,
[
"init",
"fileCreate",
"linkFileCreate",
"fileChange",
"linkFileChange",
"fileModifiedTimeChange",
"linkModifiedTimeChange",
"fileDelete",
"linkFileDelete",
],
);
});
it(`recursive watchDirectory using fsEvents when linked in same folder`, async () => {
await testWatchDirectoryOperations(
sys,
fsWatch,
tableOfEvents,
watchDirectoryOperation,
`${dir}sub`,
`${link}sub`,
/*recursive*/ true,
[
"init",
"linkSubFileCreate",
"linkSubFileChange",
"linkSubModifiedTimeChange",
"linkSubFileDelete",
],
);
});
it(`recursive watchDirectory using fsEvents when links not in directory`, async () => {
await testWatchDirectoryOperations(
sys,
fsWatch,
tableOfEvents,
watchDirectoryOperation,
`${dir}parallel`,
`${link}parallel`,
/*recursive*/ true,
[
"init",
"parallelFileCreate",
"parallelLinkFileCreate",
"parallelFileChange",
"parallelLinkFileChange",
"parallelFileModifiedTimeChange",
"parallelLinkModifiedTimeChange",
"parallelFileDelete",
"parallelLinkFileDelete",
],
);
});
function watchDirectoryOperation(
opType: keyof RecursiveFsEventsForWatchDirectory,
dir: string,
link: string,
) {
switch (opType) {
case "init":
sys.writeFile(`${dir}/sub/folder/init.ts`, "export const x = 100;");
sys.writeFile(`${dir}2/sub/folder/init.ts`, "export const x = 100;");
break;
case "fileCreate":
case "linkFileCreate":
case "linkSubFileCreate":
case "parallelFileCreate":
case "parallelLinkFileCreate":
sys.writeFile(fileName(dir, link, opType), "export const x = 100;");
break;
case "fileChange":
case "linkFileChange":
case "linkSubFileChange":
case "parallelFileChange":
case "parallelLinkFileChange":
sys.writeFile(fileName(dir, link, opType), "export const x2 = 100;");
break;
case "fileModifiedTimeChange":
case "linkModifiedTimeChange":
case "linkSubModifiedTimeChange":
case "parallelFileModifiedTimeChange":
case "parallelLinkModifiedTimeChange":
sys.setModifiedTime!(fileName(dir, link, opType), new Date());
break;
case "fileDelete":
case "linkFileDelete":
case "linkSubFileDelete":
case "parallelFileDelete":
case "parallelLinkFileDelete":
sys.deleteFile!(fileName(dir, link, opType));
break;
}
}
function fileName(dir: string, link: string, opType: string) {
return ts.startsWith(opType, "file") ?
`${dir}/sub/folder/file1.ts` :
ts.startsWith(opType, "linkSub") ?
`${dir}/linkedsub/folder/file3.ts` :
ts.startsWith(opType, "link") ?
`${link}/sub/folder/file2.ts` :
ts.startsWith(opType, "parallelFile") ?
`${dir}2/sub/folder/file4.ts` :
`${dir}/linkedsub2/sub/folder/file5.ts`;
}
}
type EventRecord = Record<string, readonly ExpectedEventAndFileName[] | undefined>;
type Operation<Events extends EventRecord> = (opType: keyof Events, dir: string, link: string) => void;
async function testWatchDirectoryOperations<System extends ts.System, Events extends EventRecord>(
sys: System,
fsWatch: FsWatch<System>,
tableOfEvents: Events,
operation: Operation<Events>,
directoryName: string,
linkName: string,
recursive: boolean,
opTypes: (keyof Events & string)[],
) {
const dirResult = watchDirectory(sys, fsWatch, directoryName, recursive);
const linkResult = watchDirectory(sys, fsWatch, linkName, recursive);
for (const opType of opTypes) {
await watchDirectoryOperation(tableOfEvents, opType, operation, directoryName, linkName, dirResult, linkResult);
}
dirResult.watcher.close();
linkResult.watcher.close();
}
async function watchDirectoryOperation<Events extends EventRecord>(
tableOfEvents: Events,
opType: keyof Events & string,
operation: Operation<Events>,
directoryName: string,
linkName: string,
dirResult: WatchDirectoryResult,
linkResult: WatchDirectoryResult,
) {
initializeWatchDirectoryResult(dirResult, linkResult);
operation(opType, directoryName, linkName);
await verfiyWatchDirectoryResult(
opType,
dirResult,
linkResult,
tableOfEvents[opType],
);
}
function getFileName(): (dir: string) => string {
return dir => `${dir}/${ts.getBaseFileName(dir)}.ts`;
}
describe("with ts.sys::", () => {
const root = ts.normalizePath(IO.joinPath(IO.getWorkspaceRoot(), "tests/baselines/symlinks"));
const osFlavor = process.platform === "darwin" ?
TestServerHostOsFlavor.MacOs :
process.platform === "win32" ?
TestServerHostOsFlavor.Windows :
TestServerHostOsFlavor.Linux;
before(() => {
cleanup();
});
after(() => {
cleanup();
});
function cleanup() {
withSwallowException(() => fs.rmSync(root, { recursive: true, force: true }));
}
function withSwallowException(op: () => void) {
try {
op();
}
catch { /* swallow */ }
}
describe("watchFile using polling", () => {
before(() => {
ts.sys.writeFile(`${root}/polling/file.ts`, "export const x = 10;");
withSwallowException(() => fs.symlinkSync(`${root}/polling`, `${root}/linkedpolling`, "junction"));
});
verifyWatchFile(
"watchFile using polling",
ts.sys,
`${root}/polling/file.ts`,
`${root}/linkedpolling/file.ts`,
{ watchFile: ts.WatchFileKind.PriorityPollingInterval },
);
});
describe("watchFile using fsEvents", () => {
before(() => {
ts.sys.writeFile(`${root}/fsevents/file.ts`, "export const x = 10;");
withSwallowException(() => fs.symlinkSync(`${root}/fsevents`, `${root}/linkedfsevents`, "junction"));
});
verifyWatchFile(
"watchFile using fsEvents",
ts.sys,
`${root}/fsevents/file.ts`,
`${root}/linkedfsevents/file.ts`,
{ watchFile: ts.WatchFileKind.UseFsEvents },
);
});
describe("watchDirectory using polling", () => {
before(() => {
ts.sys.writeFile(`${root}/dirpolling/file.ts`, "export const x = 10;");
withSwallowException(() => fs.symlinkSync(`${root}/dirpolling`, `${root}/linkeddirpolling`, "junction"));
});
verifyWatchFile(
"watchDirectory using polling",
ts.sys,
`${root}/dirpolling`,
`${root}/linkeddirpolling`,
{ watchFile: ts.WatchFileKind.PriorityPollingInterval },
getFileName(),
);
});
describe("watchDirectory using fsEvents", () => {
before(() => {
ts.sys.writeFile(`${root}/dirfsevents/file.ts`, "export const x = 10;");
withSwallowException(() => fs.symlinkSync(`${root}/dirfsevents`, `${root}/linkeddirfsevents`, "junction"));
});
verifyWatchDirectoryUsingFsEvents(
ts.sys,
(dir, _recursive, cb) => fs.watch(dir, { persistent: true }, cb),
`${root}/dirfsevents`,
`${root}/linkeddirfsevents`,
osFlavor,
);
});
if (osFlavor !== TestServerHostOsFlavor.Linux) {
describe("recursive watchDirectory using fsEvents", () => {
before(() => {
setupRecursiveFsEvents("recursivefsevents");
setupRecursiveFsEvents("recursivefseventssub");
setupRecursiveFsEvents("recursivefseventsparallel");
});
verifyRecursiveWatchDirectoryUsingFsEvents(
ts.sys,
(dir, recursive, cb) => fs.watch(dir, { persistent: true, recursive }, cb),
`${root}/recursivefsevents`,
`${root}/linkedrecursivefsevents`,
osFlavor,
);
});
}
function setupRecursiveFsEvents(recursiveName: string) {
ts.sys.writeFile(`${root}/${recursiveName}/sub/folder/file.ts`, "export const x = 10;");
ts.sys.writeFile(`${root}/${recursiveName}2/sub/folder/file.ts`, "export const x = 10;");
withSwallowException(() => fs.symlinkSync(`${root}/${recursiveName}`, `${root}/linked${recursiveName}`, "junction"));
withSwallowException(() => fs.symlinkSync(`${root}/${recursiveName}/sub`, `${root}/${recursiveName}/linkedsub`, "junction"));
withSwallowException(() => fs.symlinkSync(`${root}/${recursiveName}2`, `${root}/${recursiveName}/linkedsub2`, "junction"));
}
});
describe("with virtualFileSystem::", () => {
const root = ts.normalizePath("/tests/baselines/symlinks");
function getSys(osFlavor?: TestServerHostOsFlavor) {
return createWatchedSystem({
[`${root}/folder/file.ts`]: "export const x = 10;",
[`${root}/linked`]: { symLink: `${root}/folder` },
}, { osFlavor });
}
verifyWatchFile(
"watchFile using polling",
getSys(),
`${root}/folder/file.ts`,
`${root}/linked/file.ts`,
{ watchFile: ts.WatchFileKind.PriorityPollingInterval },
);
verifyWatchFile(
"watchFile using fsEvents",
getSys(),
`${root}/folder/file.ts`,
`${root}/linked/file.ts`,
{ watchFile: ts.WatchFileKind.UseFsEvents },
);
verifyWatchFile(
"watchDirectory using polling",
getSys(),
`${root}/folder`,
`${root}/linked`,
{ watchFile: ts.WatchFileKind.PriorityPollingInterval },
getFileName(),
);
function verifyWatchDirectoryUsingFsEventsTestServerHost(osFlavor: TestServerHostOsFlavor) {
verifyWatchDirectoryUsingFsEvents(
getSys(osFlavor),
(dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb),
`${root}/folder`,
`${root}/linked`,
osFlavor,
);
}
verifyWatchDirectoryUsingFsEventsTestServerHost(TestServerHostOsFlavor.Windows);
verifyWatchDirectoryUsingFsEventsTestServerHost(TestServerHostOsFlavor.MacOs);
verifyWatchDirectoryUsingFsEventsTestServerHost(TestServerHostOsFlavor.Linux);
function getRecursiveSys(osFlavor: TestServerHostOsFlavor) {
return createWatchedSystem({
...getRecursiveFs("recursivefsevents"),
...getRecursiveFs("recursivefseventssub"),
...getRecursiveFs("recursivefseventsparallel"),
}, { osFlavor });
function getRecursiveFs(recursiveName: string): FileOrFolderOrSymLinkMap {
return {
[`${root}/${recursiveName}/sub/folder/file.ts`]: "export const x = 10;",
[`${root}/${recursiveName}2/sub/folder/file.ts`]: "export const x = 10;",
[`${root}/linked${recursiveName}`]: { symLink: `${root}/${recursiveName}` },
[`${root}/${recursiveName}/linkedsub`]: { symLink: `${root}/${recursiveName}/sub` },
[`${root}/${recursiveName}/linkedsub2`]: { symLink: `${root}/${recursiveName}2` },
};
}
}
function verifyRecursiveWatchDirectoryUsingFsEventsTestServerHost(osFlavor: TestServerHostOsFlavor.Windows | TestServerHostOsFlavor.MacOs) {
verifyRecursiveWatchDirectoryUsingFsEvents(
getRecursiveSys(osFlavor),
(dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb),
`${root}/recursivefsevents`,
`${root}/linkedrecursivefsevents`,
osFlavor,
);
}
verifyRecursiveWatchDirectoryUsingFsEventsTestServerHost(TestServerHostOsFlavor.Windows);
verifyRecursiveWatchDirectoryUsingFsEventsTestServerHost(TestServerHostOsFlavor.MacOs);
});
});

View File

@@ -188,7 +188,7 @@ a;b;
verifyTscWatch({
scenario: "forceConsistentCasingInFileNames",
subScenario,
commandLineArgs: ["--w", "--p", ".", "--explainFiles"],
commandLineArgs: ["--w", "--p", ".", "--explainFiles", "--extendedDiagnostics"],
sys: () => {
const moduleA: File = {
path: diskPath,
@@ -226,7 +226,6 @@ a;b;
`,
),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
symlinksNotReflected: [`/user/username/projects/myproject/link.ts`],
},
],
});
@@ -236,13 +235,13 @@ a;b;
verifyFileSymlink("when file symlink target matches disk but import does not", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/Xy.ts`, `./XY`);
verifyFileSymlink("when import matches disk but file symlink target does not", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/XY.ts`, `./Xy`);
verifyFileSymlink("when import and file symlink target agree but do not match disk", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/Xy.ts`, `./Xy`);
verifyFileSymlink("when import, file symlink target, and disk are all different", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/Xy.ts`, `./yX`);
verifyFileSymlink("when import, file symlink target, and disk are all different", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/Xy.ts`, `./xY`);
function verifyDirSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) {
verifyTscWatch({
scenario: "forceConsistentCasingInFileNames",
subScenario,
commandLineArgs: ["--w", "--p", ".", "--explainFiles"],
commandLineArgs: ["--w", "--p", ".", "--explainFiles", "--extendedDiagnostics"],
sys: () => {
const moduleA: File = {
path: `${diskPath}/a.ts`,
@@ -281,7 +280,6 @@ a;b;
`,
),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
symlinksNotReflected: [`/user/username/projects/myproject/link/a.ts`],
},
],
});
@@ -291,7 +289,7 @@ a;b;
verifyDirSymlink("when directory symlink target matches disk but import does not", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/Xy`, `./XY`);
verifyDirSymlink("when import matches disk but directory symlink target does not", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/XY`, `./Xy`);
verifyDirSymlink("when import and directory symlink target agree but do not match disk", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/Xy`, `./Xy`);
verifyDirSymlink("when import, directory symlink target, and disk are all different", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/Xy`, `./yX`);
verifyDirSymlink("when import, directory symlink target, and disk are all different", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/Xy`, `./xY`);
verifyTscWatch({
scenario: "forceConsistentCasingInFileNames",

View File

@@ -710,7 +710,7 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po
edits: [
{
caption: "emulate access",
edit: sys => sys.invokeFsWatches("/user/username/projects/myproject/main.ts", "change", /*modifiedTime*/ undefined, /*useTildeSuffix*/ undefined),
edit: sys => sys.invokeFsWatches("/user/username/projects/myproject/main.ts", "change", "/user/username/projects/myproject/main.ts", /*useTildeSuffix*/ undefined),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
},
{
@@ -744,7 +744,7 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po
},
{
caption: "receive another change event without modifying the file",
edit: sys => sys.invokeFsWatches("/user/username/projects/project/main.ts", "change", /*modifiedTime*/ undefined, /*useTildeSuffix*/ undefined),
edit: sys => sys.invokeFsWatches("/user/username/projects/project/main.ts", "change", "/user/username/projects/project/main.ts", /*useTildeSuffix*/ undefined),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
},
{
@@ -754,7 +754,7 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po
},
{
caption: "receive another change event without modifying the file",
edit: sys => sys.invokeFsWatches("/user/username/projects/project/main.ts", "change", /*modifiedTime*/ undefined, /*useTildeSuffix*/ undefined),
edit: sys => sys.invokeFsWatches("/user/username/projects/project/main.ts", "change", "/user/username/projects/project/main.ts", /*useTildeSuffix*/ undefined),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
},
],

View File

@@ -43,6 +43,7 @@ describe("unittests:: tsserver:: events:: watchEvents", () => {
"Custom WatchedFiles",
"Custom WatchedDirectories",
host.getCanonicalFileName,
host,
),
watchFile,
watchDirectory,

View File

@@ -1,4 +1,7 @@
import * as ts from "../../_namespaces/ts";
import {
dedent,
} from "../../_namespaces/Utils";
import {
jsonToReadableText,
} from "../helpers";
@@ -14,6 +17,7 @@ import {
createServerHost,
File,
libFile,
SymLink,
} from "../helpers/virtualFileSystemWithWatch";
describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => {
@@ -150,4 +154,182 @@ describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => {
verifyGetErrRequest({ session, files: [anotherFile] });
baselineTsserverLogs("forceConsistentCasingInFileNames", "when changing module name with different casing", session);
});
describe("with symlinks", () => {
function verifySymlink(
subScenario: string,
linkPath: string,
getFiles: () => { moduleA: File; symlinkA: SymLink; moduleB: File; tsconfig: File; },
) {
it(subScenario, () => {
const { moduleA, symlinkA, moduleB, tsconfig } = getFiles();
const host = createServerHost([moduleA, symlinkA, moduleB, libFile, tsconfig], {
currentDirectory: "/user/username/projects/myproject",
});
const session = new TestSession(host);
openFilesForSession([moduleB], session);
verifyGetErrRequest({
session,
files: [moduleB],
});
host.prependFile(moduleA.path, `// some comment\n`);
verifyGetErrRequest({
session,
files: [moduleB],
existingTimeouts: true,
});
baselineTsserverLogs("forceConsistentCasingInFileNames", subScenario, session);
});
it(`${subScenario} with target open`, () => {
const { moduleA, symlinkA, moduleB, tsconfig } = getFiles();
const host = createServerHost([moduleA, symlinkA, moduleB, libFile, tsconfig], {
currentDirectory: "/user/username/projects/myproject",
});
const session = new TestSession(host);
openFilesForSession([moduleB, moduleA], session);
verifyGetErrRequest({
session,
files: [moduleB],
});
session.executeCommandSeq<ts.server.protocol.ChangeRequest>({
command: ts.server.protocol.CommandTypes.Change,
arguments: {
file: moduleA.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `// some comment\n`,
},
});
verifyGetErrRequest({
session,
files: [moduleB],
});
baselineTsserverLogs("forceConsistentCasingInFileNames", `${subScenario} with target open`, session);
});
it(`${subScenario} with link open`, () => {
const { moduleA, symlinkA, moduleB, tsconfig } = getFiles();
const host = createServerHost([moduleA, symlinkA, moduleB, libFile, tsconfig], {
currentDirectory: "/user/username/projects/myproject",
});
const session = new TestSession(host);
openFilesForSession([moduleB, linkPath], session);
verifyGetErrRequest({
session,
files: [moduleB],
});
host.prependFile(moduleA.path, `// some comment\n`);
verifyGetErrRequest({
session,
files: [moduleB],
existingTimeouts: true,
});
baselineTsserverLogs("forceConsistentCasingInFileNames", `${subScenario} with link open`, session);
});
it(`${subScenario} with target and link open`, () => {
const { moduleA, symlinkA, moduleB, tsconfig } = getFiles();
const host = createServerHost([moduleA, symlinkA, moduleB, libFile, tsconfig], {
currentDirectory: "/user/username/projects/myproject",
});
const session = new TestSession(host);
openFilesForSession([moduleB, moduleA, linkPath], session);
verifyGetErrRequest({
session,
files: [moduleB],
});
session.executeCommandSeq<ts.server.protocol.ChangeRequest>({
command: ts.server.protocol.CommandTypes.Change,
arguments: {
file: moduleA.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `// some comment\n`,
},
});
verifyGetErrRequest({
session,
files: [moduleB],
});
baselineTsserverLogs("forceConsistentCasingInFileNames", `${subScenario} with target and link open`, session);
});
}
function verifyFileSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) {
verifySymlink(subScenario, `/user/username/projects/myproject/link.ts`, () => {
const moduleA: File = {
path: diskPath,
content: dedent`
export const a = 1;
export const b = 2;
`,
};
const symlinkA: SymLink = {
path: `/user/username/projects/myproject/link.ts`,
symLink: targetPath,
};
const moduleB: File = {
path: `/user/username/projects/myproject/b.ts`,
content: dedent`
import { a } from "${importedPath}";
import { b } from "./link";
a;b;
`,
};
const tsconfig: File = {
path: `/user/username/projects/myproject/tsconfig.json`,
content: jsonToReadableText({ compilerOptions: { forceConsistentCasingInFileNames: true } }),
};
return { moduleA, symlinkA, moduleB, tsconfig };
});
}
verifyFileSymlink("when both file symlink target and import match disk", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/XY.ts`, `./XY`);
verifyFileSymlink("when file symlink target matches disk but import does not", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/Xy.ts`, `./XY`);
verifyFileSymlink("when import matches disk but file symlink target does not", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/XY.ts`, `./Xy`);
verifyFileSymlink("when import and file symlink target agree but do not match disk", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/Xy.ts`, `./Xy`);
verifyFileSymlink("when import, file symlink target, and disk are all different", `/user/username/projects/myproject/XY.ts`, `/user/username/projects/myproject/Xy.ts`, `./xY`);
function verifyDirSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) {
verifySymlink(subScenario, `/user/username/projects/myproject/link/a.ts`, () => {
const moduleA: File = {
path: `${diskPath}/a.ts`,
content: dedent`
export const a = 1;
export const b = 2;
`,
};
const symlinkA: SymLink = {
path: `/user/username/projects/myproject/link`,
symLink: targetPath,
};
const moduleB: File = {
path: `/user/username/projects/myproject/b.ts`,
content: dedent`
import { a } from "${importedPath}/a";
import { b } from "./link/a";
a;b;
`,
};
const tsconfig: File = {
path: `/user/username/projects/myproject/tsconfig.json`,
// Use outFile because otherwise the real and linked files will have the same output path
content: jsonToReadableText({ compilerOptions: { forceConsistentCasingInFileNames: true, outFile: "out.js", module: "system" } }),
};
return { moduleA, symlinkA, moduleB, tsconfig };
});
}
verifyDirSymlink("when both directory symlink target and import match disk", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/XY`, `./XY`);
verifyDirSymlink("when directory symlink target matches disk but import does not", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/Xy`, `./XY`);
verifyDirSymlink("when import matches disk but directory symlink target does not", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/XY`, `./Xy`);
verifyDirSymlink("when import and directory symlink target agree but do not match disk", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/Xy`, `./Xy`);
verifyDirSymlink("when import, directory symlink target, and disk are all different", `/user/username/projects/myproject/XY`, `/user/username/projects/myproject/Xy`, `./xY`);
});
});

View File

@@ -85,7 +85,7 @@ describe("unittests:: tsserver:: moduleResolution", () => {
}),
{ ignoreWatches: true },
);
host.invokeFsWatches(packageFile.path, "rename", /*modifiedTime*/ undefined, /*useTildeSuffix*/ undefined); // Create event instead of change
host.invokeFsWatches(packageFile.path, "rename", packageFile.path, /*useTildeSuffix*/ undefined); // Create event instead of change
host.runQueuedTimeoutCallbacks(); // Failed lookup updates
host.runQueuedTimeoutCallbacks(); // Actual update
verifyErr();