Skip to content

Commit 26358d2

Browse files
authored
Make sure virtual file system with watch behaves same way as sys/node so we have proper test coverage for symlinks (microsoft#57607)
1 parent 075ebb4 commit 26358d2

File tree

65 files changed

+23475
-221
lines changed

Some content is hidden

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

65 files changed

+23475
-221
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(),

0 commit comments

Comments
 (0)