mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-04-17 01:49:41 -05:00
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:
@@ -398,6 +398,7 @@ class SessionServerHost implements ts.server.ServerHost {
|
||||
"watchedFiles",
|
||||
"watchedDirectories",
|
||||
ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames),
|
||||
this,
|
||||
);
|
||||
|
||||
constructor(private host: NativeLanguageServiceHost) {
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
788
src/testRunner/unittests/sys/symlinkWatching.ts
Normal file
788
src/testRunner/unittests/sys/symlinkWatching.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("unittests:: tsserver:: events:: watchEvents", () => {
|
||||
"Custom WatchedFiles",
|
||||
"Custom WatchedDirectories",
|
||||
host.getCanonicalFileName,
|
||||
host,
|
||||
),
|
||||
watchFile,
|
||||
watchDirectory,
|
||||
|
||||
@@ -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`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user