Skip to content

Commit

Permalink
Verify program structure from watch edits
Browse files Browse the repository at this point in the history
  • Loading branch information
sheetalkamat committed Jun 29, 2023
1 parent b262f65 commit 2d555d4
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 18 deletions.
2 changes: 2 additions & 0 deletions src/compiler/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface HasInvalidatedFromResolutionCache {
* @internal
*/
export interface ResolutionCache {
rootDirForResolution: string;
resolvedModuleNames: Map<Path, ModeAwareCache<CachedResolvedModuleWithFailedLookupLocations>>;
resolvedTypeReferenceDirectives: Map<Path, ModeAwareCache<CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations>>;
resolvedLibraries: Map<string, CachedResolvedModuleWithFailedLookupLocations>;
Expand Down Expand Up @@ -551,6 +552,7 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
const typeRootsWatches = new Map<string, FileWatcher>();

return {
rootDirForResolution,
resolvedModuleNames,
resolvedTypeReferenceDirectives,
resolvedLibraries,
Expand Down
11 changes: 9 additions & 2 deletions src/compiler/watchPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
perfLogger,
PollingInterval,
ProjectReference,
ResolutionCache,
ResolutionCacheHost,
ResolutionMode,
ResolvedModule,
Expand Down Expand Up @@ -333,6 +334,8 @@ export interface Watch<T> {
getCurrentProgram(): T;
/** Closes the watch */
close(): void;
/** @internal */
getResolutionCache(): ResolutionCache;
}

/**
Expand Down Expand Up @@ -543,8 +546,8 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
if (configFileName) updateExtendedConfigFilesWatches(toPath(configFileName), compilerOptions, watchOptions, WatchType.ExtendedConfigFile);

return configFileName ?
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close } :
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close };
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close, getResolutionCache } :
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close, getResolutionCache };

function close() {
clearInvalidateResolutionsOfFailedLookupLocations();
Expand Down Expand Up @@ -584,6 +587,10 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
}
}

function getResolutionCache() {
return resolutionCache;
}

function getCurrentBuilderProgram() {
return builderProgram;
}
Expand Down
6 changes: 3 additions & 3 deletions src/harness/incrementalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,15 @@ function getProgramStructure(program: ts.Program | undefined) {
return baseline.join("\n");
}

function verifyProgramStructure(expectedProgram: ts.Program, actualProgram: ts.Program, projectName: string) {
export function verifyProgramStructure(expectedProgram: ts.Program, actualProgram: ts.Program, projectName: string) {
const actual = getProgramStructure(actualProgram);
const expected = getProgramStructure(expectedProgram);
ts.Debug.assert(actual === expected, `Program verification:: ${projectName}`, () => `Program Details::\nExpected:\n${expected}\nActual:\n${actual}`);
}

function verifyResolutionCache(actual: ts.ResolutionCache, actualProgram: ts.Program, resolutionHostCacheHost: ts.ResolutionCacheHost) {
export function verifyResolutionCache(actual: ts.ResolutionCache, actualProgram: ts.Program, resolutionHostCacheHost: ts.ResolutionCacheHost) {
const currentDirectory = resolutionHostCacheHost.getCurrentDirectory!();
const expected = ts.createResolutionCache(resolutionHostCacheHost, currentDirectory, /*logChangesWhenResolvingModule*/ false);
const expected = ts.createResolutionCache(resolutionHostCacheHost, actual.rootDirForResolution, /*logChangesWhenResolvingModule*/ false);
expected.startCachingPerDirectoryResolution();

type ExpectedResolution = ts.CachedResolvedModuleWithFailedLookupLocations & ts.CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations;
Expand Down
4 changes: 1 addition & 3 deletions src/testRunner/unittests/helpers/baseline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,10 @@ export function commandLineCallbacks(
};
}

export function baselinePrograms(baseline: string[], getPrograms: () => readonly CommandLineProgram[], oldPrograms: readonly (CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) {
const programs = getPrograms();
export function baselinePrograms(baseline: string[], programs: readonly CommandLineProgram[], oldPrograms: readonly (CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) {
for (let i = 0; i < programs.length; i++) {
baselineProgram(baseline, programs[i], oldPrograms[i], baselineDependencies);
}
return programs;
}

function baselineProgram(baseline: string[], [program, builderProgram]: CommandLineProgram, oldProgram: CommandLineProgram | undefined, baselineDependencies: boolean | undefined) {
Expand Down
5 changes: 3 additions & 2 deletions src/testRunner/unittests/helpers/tsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,11 @@ export function testTscCompile(input: TestTscCompile) {

function additionalBaseline(sys: TscCompileSystem) {
const { baselineSourceMap, baselineReadFileCalls, baselinePrograms: shouldBaselinePrograms, baselineDependencies } = input;
if (input.computeDtsSignatures) storeDtsSignatures(sys, getPrograms!());
const programs = getPrograms!();
if (input.computeDtsSignatures) storeDtsSignatures(sys, programs);
if (shouldBaselinePrograms) {
const baseline: string[] = [];
baselinePrograms(baseline, getPrograms!, ts.emptyArray, baselineDependencies);
baselinePrograms(baseline, programs, ts.emptyArray, baselineDependencies);
sys.write(baseline.join("\n"));
}
if (baselineReadFileCalls) {
Expand Down
86 changes: 82 additions & 4 deletions src/testRunner/unittests/helpers/tscWatch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
verifyProgramStructure,
verifyResolutionCache,
} from "../../../harness/incrementalUtils";
import { patchHostForBuildInfoReadWrite } from "../../_namespaces/fakes";
import { Baseline } from "../../_namespaces/Harness";
import * as ts from "../../_namespaces/ts";
Expand Down Expand Up @@ -34,6 +38,9 @@ export interface TscWatchCompileChange<T extends ts.BuilderProgram = ts.EmitAndS
programs: readonly CommandLineProgram[],
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 {
baselineSourceMap?: boolean;
Expand Down Expand Up @@ -212,12 +219,13 @@ export interface RunWatchBaseline<T extends ts.BuilderProgram> extends BaselineB
sys: TscWatchSystem;
getPrograms: () => readonly CommandLineProgram[];
watchOrSolution: WatchOrSolution<T>;
useSourceOfProjectReferenceRedirect?: () => boolean;
}
export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanticDiagnosticsBuilderProgram>({
scenario, subScenario, commandLineArgs,
getPrograms, sys, baseline, oldSnap,
baselineSourceMap, baselineDependencies,
edits, watchOrSolution
edits, watchOrSolution, useSourceOfProjectReferenceRedirect,
}: RunWatchBaseline<T>) {
baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`);
let programs = watchBaseline({
Expand All @@ -231,7 +239,7 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
});

if (edits) {
for (const { caption, edit, timeouts } of edits) {
for (const { caption, edit, timeouts, symlinksNotReflected, skipStructureCheck } of edits) {
oldSnap = applyEdit(sys, baseline, edit, caption);
timeouts(sys, programs, watchOrSolution);
programs = watchBaseline({
Expand All @@ -242,6 +250,9 @@ export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanti
oldSnap,
baselineSourceMap,
baselineDependencies,
resolutionCache: !skipStructureCheck ? (watchOrSolution as ts.WatchOfConfigFile<T> | undefined)?.getResolutionCache?.() : undefined,
useSourceOfProjectReferenceRedirect,
symlinksNotReflected,
});
}
}
Expand All @@ -259,20 +270,87 @@ export function isWatch(commandLineArgs: readonly string[]) {
export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions {
oldPrograms: readonly (CommandLineProgram | undefined)[];
getPrograms: () => readonly CommandLineProgram[];
resolutionCache?: ts.ResolutionCache;
useSourceOfProjectReferenceRedirect?: () => boolean;
symlinksNotReflected?: readonly string[]
}
export function watchBaseline({ baseline, getPrograms, oldPrograms, sys, oldSnap, baselineSourceMap, baselineDependencies }: WatchBaseline) {
export function watchBaseline({
baseline,
getPrograms,
oldPrograms,
sys,
oldSnap,
baselineSourceMap,
baselineDependencies,
resolutionCache,
useSourceOfProjectReferenceRedirect,
symlinksNotReflected,
}: WatchBaseline) {
if (baselineSourceMap) generateSourceMapBaselineFiles(sys);
sys.serializeOutput(baseline);
const programs = baselinePrograms(baseline, getPrograms, oldPrograms, baselineDependencies);
const programs = getPrograms();
baselinePrograms(baseline, programs, oldPrograms, baselineDependencies);
sys.serializeWatches(baseline);
baseline.push(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}`, "");
sys.diff(baseline, oldSnap);
sys.writtenFiles.forEach((value, key) => {
assert.equal(value, 1, `Expected to write file ${key} only once`);
});
// 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(sys, programs[0][0], resolutionCache, useSourceOfProjectReferenceRedirect, symlinksNotReflected);
}
sys.writtenFiles.clear();
return programs;
}
function verifyProgramStructureAndResolutionCache(
sys: TscWatchSystem,
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(),
options,
projectReferences: program.getProjectReferences(),
host: compilerHost,
}), program, options.configFilePath || JSON.stringify(program.getRootFileNames()));
verifyResolutionCache(resolutionCache, program, {
...compilerHost,

getCompilerHost: () => compilerHost,
toPath: fileName => sys.toPath(fileName),
getCompilationSettings: () => options,
fileIsOpen: ts.returnFalse,
getCurrentProgram: () => program,

watchDirectoryOfFailedLookupLocation: ts.returnNoopFileWatcher,
watchAffectingFileLocation: ts.returnNoopFileWatcher,
onInvalidatedResolution: ts.noop,
watchTypeRootsDirectory: ts.returnNoopFileWatcher,
onChangedAutomaticTypeDirectiveNames: ts.noop,
scheduleInvalidateResolutionsOfFailedLookupLocations: ts.noop,
getCachedDirectoryStructureHost: ts.returnUndefined,
writeLog: ts.noop,
});
}
export interface VerifyTscWatch extends TscWatchCompile {
baselineIncremental?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
private fs: Map<Path, FSEntry> = new Map();
private time = timeIncrements;
getCanonicalFileName: (s: string) => string;
private toPath: (f: string) => Path;
toPath: (f: string) => Path;
readonly timeoutCallbacks = new Callbacks(this, "Timeout");
readonly immediateCallbacks = new Callbacks(this, "Immedidate");
readonly screenClears: number[] = [];
Expand Down
2 changes: 1 addition & 1 deletion src/testRunner/unittests/tsbuild/publicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function f22() { } // trailing`,
sys.exit(exitStatus);
sys.write(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}\n`);
const baseline: string[] = [];
baselinePrograms(baseline, getPrograms, ts.emptyArray, /*baselineDependencies*/ false);
baselinePrograms(baseline, getPrograms(), ts.emptyArray, /*baselineDependencies*/ false);
sys.write(baseline.join("\n"));
fs.makeReadonly();
sys.baseLine = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ a;b;
edit: sys => sys.prependFile(diskPath, `// some comment
`),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
symlinksNotReflected: [`/user/username/projects/myproject/link.ts`]
}
],
});
Expand Down Expand Up @@ -262,6 +263,7 @@ a;b;
edit: sys => sys.prependFile(`${diskPath}/a.ts`, `// some comment
`),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
symlinksNotReflected: [`/user/username/projects/myproject/link/a.ts`]
}
],
});
Expand Down
4 changes: 4 additions & 0 deletions src/testRunner/unittests/tscWatch/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ declare module "fs" {
}
`),
timeouts: sys => sys.runQueuedTimeoutCallbacks(),
// This is currently issue with ambient modules in same file not leading to resolution watching
// In this case initially resolution is watched and will continued to be watched but
// incremental check will determine that the resolution should not be watched as thats what would have happened if we had started tsc --watch at this state.
skipStructureCheck: true,
}
]
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ describe("unittests:: tsc-watch:: watchAPI:: with sourceOfProjectReferenceRedire
baseline,
oldSnap,
getPrograms,
watchOrSolution: watch
watchOrSolution: watch,
useSourceOfProjectReferenceRedirect: ts.returnTrue,
});
}

Expand Down
3 changes: 2 additions & 1 deletion src/testRunner/unittests/tscWatch/watchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,8 @@ describe("unittests:: tsc-watch:: watchAPI:: when getParsedCommandLine is implem
timeouts: sys => sys.logTimeoutQueueLength(),
},
],
watchOrSolution: watch
watchOrSolution: watch,
useSourceOfProjectReferenceRedirect: ts.returnTrue,
});
});

Expand Down

0 comments on commit 2d555d4

Please sign in to comment.