Skip to content

Commit 6c79316

Browse files
committed
Tests for file watching behaviour and make sure that virtual file system with watch behaves same way
1 parent f3f70df commit 6c79316

File tree

61 files changed

+23410
-180
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+23410
-180
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ tests/baselines/rwc/*
1616
tests/baselines/reference/projectOutput/*
1717
tests/baselines/local/projectOutput/*
1818
tests/baselines/reference/testresults.tap
19+
tests/baselines/symlinks/*
1920
tests/services/baselines/prototyping/local/*
2021
tests/services/browser/typescriptServices.js
2122
src/harness/*.js

src/harness/harnessLanguageService.ts

+1
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ class SessionServerHost implements ts.server.ServerHost {
398398
"watchedFiles",
399399
"watchedDirectories",
400400
ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames),
401+
this,
401402
);
402403

403404
constructor(private host: NativeLanguageServiceHost) {

src/harness/watchUtils.ts

+55-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
addRange,
32
arrayFrom,
43
compareStringsCaseSensitive,
54
contains,
@@ -10,6 +9,7 @@ import {
109
GetCanonicalFileName,
1110
MultiMap,
1211
PollingInterval,
12+
System,
1313
} from "./_namespaces/ts";
1414

1515
export interface TestFileWatcher {
@@ -25,7 +25,7 @@ export interface TestFsWatcher<DirCallback> {
2525
export interface Watches<Data> {
2626
add(path: string, data: Data): void;
2727
remove(path: string, data: Data): void;
28-
forEach(path: string, cb: (data: Data) => void): void;
28+
forEach(path: string, cb: (data: Data, path: string) => void): void;
2929
serialize(baseline: string[]): void;
3030
}
3131

@@ -44,6 +44,7 @@ export function createWatchUtils<PollingWatcherData, FsWatcherData>(
4444
pollingWatchesName: string,
4545
fsWatchesName: string,
4646
getCanonicalFileName: GetCanonicalFileName,
47+
system: Required<Pick<System, "realpath">>,
4748
): WatchUtils<PollingWatcherData, FsWatcherData> {
4849
const pollingWatches = initializeWatches<PollingWatcherData>(pollingWatchesName);
4950
const fsWatches = initializeWatches<FsWatcherData>(fsWatchesName);
@@ -64,6 +65,8 @@ export function createWatchUtils<PollingWatcherData, FsWatcherData>(
6465
const actuals = createMultiMap<string, Data>();
6566
let serialized: Map<string, Data[]> | undefined;
6667
let canonicalPathsToStrings: Map<string, Set<string>> | undefined;
68+
let realToLinked: MultiMap<string, string> | undefined;
69+
let pathToReal: Map<string, string> | undefined;
6770
return {
6871
add,
6972
remove,
@@ -73,40 +76,69 @@ export function createWatchUtils<PollingWatcherData, FsWatcherData>(
7376

7477
function add(path: string, data: Data) {
7578
actuals.add(path, data);
76-
if (actuals.get(path)!.length === 1) {
77-
const canonicalPath = getCanonicalFileName(path);
78-
if (canonicalPath !== path) {
79-
(canonicalPathsToStrings ??= new Map()).set(
80-
canonicalPath,
81-
(canonicalPathsToStrings?.get(canonicalPath) ?? new Set()).add(path),
82-
);
83-
}
79+
if (actuals.get(path)!.length !== 1) return;
80+
const canonicalPath = getCanonicalFileName(path);
81+
if (canonicalPath !== path) {
82+
(canonicalPathsToStrings ??= new Map()).set(
83+
canonicalPath,
84+
(canonicalPathsToStrings?.get(canonicalPath) ?? new Set()).add(path),
85+
);
86+
}
87+
const real = system.realpath(path);
88+
(pathToReal ??= new Map()).set(path, real);
89+
if (real === path) return;
90+
const canonicalReal = getCanonicalFileName(real);
91+
if (getCanonicalFileName(path) !== canonicalReal) {
92+
(realToLinked ??= createMultiMap()).add(canonicalReal, path);
8493
}
8594
}
8695

8796
function remove(path: string, data: Data) {
8897
actuals.remove(path, data);
89-
if (!actuals.has(path)) {
90-
const canonicalPath = getCanonicalFileName(path);
91-
if (canonicalPath !== path) {
92-
const existing = canonicalPathsToStrings!.get(canonicalPath);
93-
if (existing!.size === 1) canonicalPathsToStrings!.delete(canonicalPath);
94-
else existing!.delete(path);
95-
}
98+
if (actuals.has(path)) return;
99+
const canonicalPath = getCanonicalFileName(path);
100+
if (canonicalPath !== path) {
101+
const existing = canonicalPathsToStrings!.get(canonicalPath);
102+
if (existing!.size === 1) canonicalPathsToStrings!.delete(canonicalPath);
103+
else existing!.delete(path);
104+
}
105+
const real = pathToReal?.get(path)!;
106+
pathToReal!.delete(path);
107+
if (real === path) return;
108+
const canonicalReal = getCanonicalFileName(real);
109+
if (getCanonicalFileName(path) !== canonicalReal) {
110+
realToLinked!.remove(canonicalReal, path);
96111
}
97112
}
98113

99-
function forEach(path: string, cb: (data: Data) => void) {
100-
let allData: Data[] | undefined;
101-
allData = addRange(allData, actuals.get(path));
114+
function getAllData(path: string) {
115+
let allData: Map<string, Data[]> | undefined;
116+
addData(path);
102117
const canonicalPath = getCanonicalFileName(path);
103-
if (canonicalPath !== path) allData = addRange(allData, actuals.get(canonicalPath));
118+
if (canonicalPath !== path) addData(canonicalPath);
104119
canonicalPathsToStrings?.get(canonicalPath)?.forEach(canonicalSamePath => {
105120
if (canonicalSamePath !== path && canonicalSamePath !== canonicalPath) {
106-
allData = addRange(allData, actuals.get(canonicalSamePath));
121+
addData(canonicalSamePath);
107122
}
108123
});
109-
allData?.forEach(cb);
124+
return allData;
125+
function addData(path: string) {
126+
const data = actuals.get(path);
127+
if (data) (allData ??= new Map()).set(path, data);
128+
}
129+
}
130+
131+
function forEach(path: string, cb: (data: Data, path: string) => void) {
132+
const real = system.realpath(path);
133+
const canonicalPath = getCanonicalFileName(path);
134+
const canonicalReal = getCanonicalFileName(real);
135+
let allData = canonicalPath === canonicalReal ? getAllData(path) : getAllData(real);
136+
realToLinked?.get(canonicalReal)?.forEach(linked => {
137+
if (allData?.has(linked)) return;
138+
const data = actuals.get(linked);
139+
if (data) (allData ??= new Map()).set(linked, data);
140+
});
141+
allData?.forEach((data, path) => data.forEach(d => cb(d, path)));
110142
}
111143

112144
function serialize(baseline: string[]) {

src/testRunner/tests.ts

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import "./unittests/services/preProcessFile";
6565
import "./unittests/services/textChanges";
6666
import "./unittests/services/transpile";
6767
import "./unittests/services/utilities";
68+
import "./unittests/sys/symlinkWatching";
6869
import "./unittests/tsbuild/amdModulesWithOut";
6970
import "./unittests/tsbuild/clean";
7071
import "./unittests/tsbuild/commandLine";

src/testRunner/unittests/helpers/tscWatch.ts

+8-17
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export interface TscWatchCompileChange<T extends ts.BuilderProgram = ts.EmitAndS
4545
watchOrSolution: WatchOrSolution<T>,
4646
) => void;
4747
// TODO:: sheetal: Needing these fields are technically issues that need to be fixed later
48-
symlinksNotReflected?: readonly string[];
4948
skipStructureCheck?: true;
5049
}
5150
export interface TscWatchCheckOptions {
@@ -220,7 +219,7 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
220219
});
221220

222221
if (edits) {
223-
for (const { caption, edit, timeouts, symlinksNotReflected, skipStructureCheck } of edits) {
222+
for (const { caption, edit, timeouts, skipStructureCheck } of edits) {
224223
applyEdit(sys, baseline, edit, caption);
225224
timeouts(sys, programs, watchOrSolution);
226225
programs = watchBaseline({
@@ -233,7 +232,6 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
233232
caption,
234233
resolutionCache: !skipStructureCheck ? (watchOrSolution as ts.WatchOfConfigFile<T> | undefined)?.getResolutionCache?.() : undefined,
235234
useSourceOfProjectReferenceRedirect,
236-
symlinksNotReflected,
237235
});
238236
}
239237
}
@@ -254,7 +252,6 @@ export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions {
254252
caption?: string;
255253
resolutionCache?: ts.ResolutionCache;
256254
useSourceOfProjectReferenceRedirect?: () => boolean;
257-
symlinksNotReflected?: readonly string[];
258255
}
259256
export function watchBaseline({
260257
baseline,
@@ -266,7 +263,6 @@ export function watchBaseline({
266263
caption,
267264
resolutionCache,
268265
useSourceOfProjectReferenceRedirect,
269-
symlinksNotReflected,
270266
}: WatchBaseline) {
271267
if (baselineSourceMap) generateSourceMapBaselineFiles(sys);
272268
const programs = getPrograms();
@@ -279,7 +275,13 @@ export function watchBaseline({
279275
// Verify program structure and resolution cache when incremental edit with tsc --watch (without build mode)
280276
if (resolutionCache && programs.length) {
281277
ts.Debug.assert(programs.length === 1);
282-
verifyProgramStructureAndResolutionCache(caption!, sys, programs[0][0], resolutionCache, useSourceOfProjectReferenceRedirect, symlinksNotReflected);
278+
verifyProgramStructureAndResolutionCache(
279+
caption!,
280+
sys,
281+
programs[0][0],
282+
resolutionCache,
283+
useSourceOfProjectReferenceRedirect,
284+
);
283285
}
284286
return programs;
285287
}
@@ -289,23 +291,12 @@ function verifyProgramStructureAndResolutionCache(
289291
program: ts.Program,
290292
resolutionCache: ts.ResolutionCache,
291293
useSourceOfProjectReferenceRedirect?: () => boolean,
292-
symlinksNotReflected?: readonly string[],
293294
) {
294295
const options = program.getCompilerOptions();
295296
const compilerHost = ts.createCompilerHostWorker(options, /*setParentNodes*/ undefined, sys);
296297
compilerHost.trace = ts.noop;
297298
compilerHost.writeFile = ts.notImplemented;
298299
compilerHost.useSourceOfProjectReferenceRedirect = useSourceOfProjectReferenceRedirect;
299-
const readFile = compilerHost.readFile;
300-
compilerHost.readFile = fileName => {
301-
const text = readFile.call(compilerHost, fileName);
302-
if (!ts.contains(symlinksNotReflected, fileName)) return text;
303-
// Handle symlinks that dont reflect the watch change
304-
ts.Debug.assert(sys.toPath(sys.realpath(fileName)) !== sys.toPath(fileName));
305-
const file = program.getSourceFile(fileName)!;
306-
ts.Debug.assert(file.text !== text);
307-
return file.text;
308-
};
309300
verifyProgramStructure(
310301
ts.createProgram({
311302
rootNames: program.getRootFileNames(),

src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
380380
this.environmentVariables = environmentVariables;
381381
currentDirectory = currentDirectory || "/";
382382
this.getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames);
383-
this.watchUtils = createWatchUtils("PolledWatches", "FsWatches", s => this.getCanonicalFileName(s));
383+
this.watchUtils = createWatchUtils("PolledWatches", "FsWatches", s => this.getCanonicalFileName(s), this);
384384
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
385385
this.executingFilePath = this.getHostSpecificPath(executingFilePath || getExecutingFilePathFromLibFile());
386386
this.currentDirectory = this.getHostSpecificPath(currentDirectory);
@@ -661,14 +661,14 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
661661

662662
deleteFile(filePath: string) {
663663
const path = this.toFullPath(filePath);
664-
const currentEntry = this.fs.get(path) as FsFile;
664+
const currentEntry = this.fs.get(path);
665665
Debug.assert(isFsFile(currentEntry));
666666
this.removeFileOrFolder(currentEntry);
667667
}
668668

669669
deleteFolder(folderPath: string, recursive?: boolean) {
670670
const path = this.toFullPath(folderPath);
671-
const currentEntry = this.fs.get(path) as FsFolder;
671+
const currentEntry = this.fs.get(path);
672672
Debug.assert(isFsFolder(currentEntry));
673673
if (recursive && currentEntry.entries.length) {
674674
const subEntries = currentEntry.entries.slice();
@@ -691,7 +691,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
691691
);
692692
}
693693

694-
private fsWatchWorker(
694+
fsWatchWorker(
695695
fileOrDirectory: string,
696696
recursive: boolean,
697697
cb: FsWatchCallback,
@@ -713,7 +713,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
713713
}
714714

715715
invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, modifiedTime: Date | undefined) {
716-
this.watchUtils.pollingWatches.forEach(fileFullPath, ({ cb }) => cb(fileFullPath, eventKind, modifiedTime));
716+
this.watchUtils.pollingWatches.forEach(fileFullPath, ({ cb }, fullPath) => cb(fullPath, eventKind, modifiedTime));
717717
}
718718

719719
private fsWatchCallback(watches: Watches<TestFsWatcher>, fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, entryFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
@@ -825,6 +825,10 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
825825
return this.getRealFsEntry(isFsFolder, path, fsEntry);
826826
}
827827

828+
private getRealFileOrFolder(s: string): FsFile | FsFolder | undefined {
829+
return this.getRealFsEntry((entry): entry is FsFile | FsFolder => !!entry && !isFsSymLink(entry), this.toFullPath(s));
830+
}
831+
828832
fileSystemEntryExists(s: string, entryKind: FileSystemEntryKind) {
829833
return entryKind === FileSystemEntryKind.File ? this.fileExists(s) : this.directoryExists(s);
830834
}
@@ -835,14 +839,11 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
835839
}
836840

837841
getModifiedTime(s: string) {
838-
const path = this.toFullPath(s);
839-
const fsEntry = this.fs.get(path);
840-
return (fsEntry && fsEntry.modifiedTime)!; // TODO: GH#18217
842+
return this.getRealFileOrFolder(s)?.modifiedTime;
841843
}
842844

843845
setModifiedTime(s: string, date: Date) {
844-
const path = this.toFullPath(s);
845-
const fsEntry = this.fs.get(path);
846+
const fsEntry = this.getRealFileOrFolder(s);
846847
if (fsEntry) {
847848
fsEntry.modifiedTime = date;
848849
this.invokeFileAndFsWatches(fsEntry.fullPath, FileWatcherEventKind.Changed, fsEntry.modifiedTime);

0 commit comments

Comments
 (0)