• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as ts from "../../_namespaces/ts";
2import * as fakes from "../../_namespaces/fakes";
3import * as Harness from "../../_namespaces/Harness";
4
5export const projects = `/user/username/projects`;
6export const projectRoot = `${projects}/myproject`;
7export import WatchedSystem = ts.TestFSWithWatch.TestServerHost;
8export type File = ts.TestFSWithWatch.File;
9export type SymLink = ts.TestFSWithWatch.SymLink;
10export import libFile = ts.TestFSWithWatch.libFile;
11export import createWatchedSystem = ts.TestFSWithWatch.createWatchedSystem;
12export import checkArray = ts.TestFSWithWatch.checkArray;
13export import checkOutputContains = ts.TestFSWithWatch.checkOutputContains;
14export import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain;
15
16export const commonFile1: File = {
17    path: "/a/b/commonFile1.ts",
18    content: "let x = 1"
19};
20export const commonFile2: File = {
21    path: "/a/b/commonFile2.ts",
22    content: "let y = 1"
23};
24
25export function checkProgramActualFiles(program: ts.Program, expectedFiles: readonly string[]) {
26    checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles);
27}
28
29export function getDiagnosticMessageChain(message: ts.DiagnosticMessage, args?: (string | number)[], next?: ts.DiagnosticMessageChain[]): ts.DiagnosticMessageChain {
30    let text = ts.getLocaleSpecificMessage(message);
31    if (args?.length) {
32        text = ts.formatStringFromArgs(text, args);
33    }
34    return {
35        messageText: text,
36        category: message.category,
37        code: message.code,
38        next
39    };
40}
41
42function isDiagnosticMessageChain(message: ts.DiagnosticMessage | ts.DiagnosticMessageChain): message is ts.DiagnosticMessageChain {
43    return !!(message as ts.DiagnosticMessageChain).messageText;
44}
45
46export function getDiagnosticOfFileFrom(file: ts.SourceFile | undefined, start: number | undefined, length: number | undefined, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic {
47    return {
48        file,
49        start,
50        length,
51
52        messageText: isDiagnosticMessageChain(message) ?
53            message :
54            getDiagnosticMessageChain(message, args).messageText,
55        category: message.category,
56        code: message.code,
57    };
58}
59
60export function getDiagnosticWithoutFile(message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic {
61    return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args);
62}
63
64export function getDiagnosticOfFile(file: ts.SourceFile, start: number, length: number, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic {
65    return getDiagnosticOfFileFrom(file, start, length, message, ...args);
66}
67
68export function getDiagnosticOfFileFromProgram(program: ts.Program, filePath: string, start: number, length: number, message: ts.DiagnosticMessage | ts.DiagnosticMessageChain, ...args: (string | number)[]): ts.Diagnostic {
69    return getDiagnosticOfFileFrom(program.getSourceFileByPath(ts.toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())),
70        start, length, message, ...args);
71}
72
73export function getUnknownCompilerOption(program: ts.Program, configFile: File, option: string) {
74    const quotedOption = `"${option}"`;
75    return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, ts.Diagnostics.Unknown_compiler_option_0, option);
76}
77
78export function getUnknownDidYouMeanCompilerOption(program: ts.Program, configFile: File, option: string, didYouMean: string) {
79    const quotedOption = `"${option}"`;
80    return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, ts.Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean);
81}
82
83export function getDiagnosticModuleNotFoundOfFile(program: ts.Program, file: File, moduleName: string) {
84    const quotedModuleName = `"${moduleName}"`;
85    return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, ts.Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option, moduleName);
86}
87
88export function runQueuedTimeoutCallbacks(sys: WatchedSystem) {
89    sys.runQueuedTimeoutCallbacks();
90}
91
92export function checkSingleTimeoutQueueLengthAndRun(sys: WatchedSystem) {
93    sys.checkTimeoutQueueLengthAndRun(1);
94}
95
96export function checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys: WatchedSystem) {
97    sys.checkTimeoutQueueLengthAndRun(1);
98    sys.checkTimeoutQueueLength(0);
99}
100
101export type WatchOrSolution<T extends ts.BuilderProgram> = void | ts.SolutionBuilder<T> | ts.WatchOfConfigFile<T> | ts.WatchOfFilesAndCompilerOptions<T>;
102export interface TscWatchCompileChange<T extends ts.BuilderProgram = ts.EmitAndSemanticDiagnosticsBuilderProgram> {
103    caption: string;
104    change: (sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void;
105    timeouts: (
106        sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles,
107        programs: readonly ts.CommandLineProgram[],
108        watchOrSolution: WatchOrSolution<T>
109    ) => void;
110}
111export interface TscWatchCheckOptions {
112    baselineSourceMap?: boolean;
113    baselineDependencies?: boolean;
114}
115export interface TscWatchCompileBase<T extends ts.BuilderProgram = ts.EmitAndSemanticDiagnosticsBuilderProgram> extends TscWatchCheckOptions {
116    scenario: string;
117    subScenario: string;
118    commandLineArgs: readonly string[];
119    changes: readonly TscWatchCompileChange<T>[];
120}
121export interface TscWatchCompile extends TscWatchCompileBase {
122    sys: () => WatchedSystem;
123}
124
125export const noopChange: TscWatchCompileChange = {
126    caption: "No change",
127    change: ts.noop,
128    timeouts: sys => sys.checkTimeoutQueueLength(0),
129};
130
131export type SystemSnap = ReturnType<WatchedSystem["snap"]>;
132function tscWatchCompile(input: TscWatchCompile) {
133    it("tsc-watch:: Generates files matching the baseline", () => {
134        const { sys, baseline, oldSnap } = createBaseline(input.sys());
135        const {
136            scenario, subScenario,
137            commandLineArgs, changes,
138            baselineSourceMap, baselineDependencies
139        } = input;
140
141        if (!isWatch(commandLineArgs)) sys.exit = exitCode => sys.exitCode = exitCode;
142        const { cb, getPrograms } = ts.commandLineCallbacks(sys);
143        const watchOrSolution = ts.executeCommandLine(
144            sys,
145            cb,
146            commandLineArgs,
147        );
148        runWatchBaseline({
149            scenario,
150            subScenario,
151            commandLineArgs,
152            sys,
153            baseline,
154            oldSnap,
155            getPrograms,
156            baselineSourceMap,
157            baselineDependencies,
158            changes,
159            watchOrSolution
160        });
161    });
162}
163
164export interface BaselineBase {
165    baseline: string[];
166    sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles;
167    oldSnap: SystemSnap;
168}
169
170export interface Baseline extends BaselineBase, ts.CommandLineCallbacks {
171}
172
173export function createBaseline(system: WatchedSystem, modifySystem?: (sys: WatchedSystem, originalRead: WatchedSystem["readFile"]) => void): Baseline {
174    const originalRead = system.readFile;
175    const initialSys = fakes.patchHostForBuildInfoReadWrite(system);
176    modifySystem?.(initialSys, originalRead);
177    const sys = ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(initialSys);
178    const baseline: string[] = [];
179    baseline.push("Input::");
180    sys.diff(baseline);
181    const { cb, getPrograms } = ts.commandLineCallbacks(sys);
182    return { sys, baseline, oldSnap: sys.snap(), cb, getPrograms };
183}
184
185export function createSolutionBuilderWithWatchHostForBaseline(sys: WatchedSystem, cb: ts.ExecuteCommandLineCallbacks) {
186    const host = ts.createSolutionBuilderWithWatchHost(sys,
187        /*createProgram*/ undefined,
188        ts.createDiagnosticReporter(sys, /*pretty*/ true),
189        ts.createBuilderStatusReporter(sys, /*pretty*/ true),
190        ts.createWatchStatusReporter(sys, /*pretty*/ true)
191    );
192    host.afterProgramEmitAndDiagnostics = cb;
193    host.afterEmitBundle = cb;
194    return host;
195}
196
197interface CreateWatchCompilerHostOfConfigFileForBaseline<T extends ts.BuilderProgram> extends ts.CreateWatchCompilerHostOfConfigFileInput<T> {
198    system: WatchedSystem,
199    cb: ts.ExecuteCommandLineCallbacks;
200}
201
202export function createWatchCompilerHostOfConfigFileForBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanticDiagnosticsBuilderProgram>(
203    input: CreateWatchCompilerHostOfConfigFileForBaseline<T>
204) {
205    const host = ts.createWatchCompilerHostOfConfigFile({
206        ...input,
207        reportDiagnostic: ts.createDiagnosticReporter(input.system, /*pretty*/ true),
208        reportWatchStatus: ts.createWatchStatusReporter(input.system, /*pretty*/ true),
209    });
210    updateWatchHostForBaseline(host, input.cb);
211    return host;
212}
213
214interface CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline<T extends ts.BuilderProgram> extends ts.CreateWatchCompilerHostOfFilesAndCompilerOptionsInput<T> {
215    system: WatchedSystem,
216    cb: ts.ExecuteCommandLineCallbacks;
217}
218export function createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanticDiagnosticsBuilderProgram>(
219    input: CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline<T>
220) {
221    const host = ts.createWatchCompilerHostOfFilesAndCompilerOptions({
222        ...input,
223        reportDiagnostic: ts.createDiagnosticReporter(input.system, /*pretty*/ true),
224        reportWatchStatus: ts.createWatchStatusReporter(input.system, /*pretty*/ true),
225    });
226    updateWatchHostForBaseline(host, input.cb);
227    return host;
228}
229
230function updateWatchHostForBaseline<T extends ts.BuilderProgram>(host: ts.WatchCompilerHost<T>, cb: ts.ExecuteCommandLineCallbacks) {
231    const emitFilesAndReportErrors = host.afterProgramCreate!;
232    host.afterProgramCreate = builderProgram => {
233        emitFilesAndReportErrors.call(host, builderProgram);
234        cb(builderProgram);
235    };
236    return host;
237}
238
239export function applyChange(sys: BaselineBase["sys"], baseline: BaselineBase["baseline"], change: TscWatchCompileChange["change"], caption?: TscWatchCompileChange["caption"]) {
240    const oldSnap = sys.snap();
241    baseline.push(`Change::${caption ? " " + caption : ""}`, "");
242    change(sys);
243    baseline.push("Input::");
244    sys.diff(baseline, oldSnap);
245    return sys.snap();
246}
247
248export interface RunWatchBaseline<T extends ts.BuilderProgram> extends BaselineBase, TscWatchCompileBase<T> {
249    sys: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles;
250    getPrograms: () => readonly ts.CommandLineProgram[];
251    watchOrSolution: WatchOrSolution<T>;
252}
253export function runWatchBaseline<T extends ts.BuilderProgram = ts.EmitAndSemanticDiagnosticsBuilderProgram>({
254    scenario, subScenario, commandLineArgs,
255    getPrograms, sys, baseline, oldSnap,
256    baselineSourceMap, baselineDependencies,
257    changes, watchOrSolution
258}: RunWatchBaseline<T>) {
259    baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`);
260    let programs = watchBaseline({
261        baseline,
262        getPrograms,
263        oldPrograms: ts.emptyArray,
264        sys,
265        oldSnap,
266        baselineSourceMap,
267        baselineDependencies,
268    });
269
270    for (const { caption, change, timeouts } of changes) {
271        oldSnap = applyChange(sys, baseline, change, caption);
272        timeouts(sys, programs, watchOrSolution);
273        programs = watchBaseline({
274            baseline,
275            getPrograms,
276            oldPrograms: programs,
277            sys,
278            oldSnap,
279            baselineSourceMap,
280            baselineDependencies,
281        });
282    }
283    Harness.Baseline.runBaseline(`${ts.isBuild(commandLineArgs) ? "tsbuild" : "tsc"}${isWatch(commandLineArgs) ? "Watch" : ""}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n"));
284}
285
286function isWatch(commandLineArgs: readonly string[]) {
287    return ts.forEach(commandLineArgs, arg => {
288        if (arg.charCodeAt(0) !== ts.CharacterCodes.minus) return false;
289        const option = arg.slice(arg.charCodeAt(1) === ts.CharacterCodes.minus ? 2 : 1).toLowerCase();
290        return option === "watch" || option === "w";
291    });
292}
293
294export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions {
295    oldPrograms: readonly (ts.CommandLineProgram | undefined)[];
296    getPrograms: () => readonly ts.CommandLineProgram[];
297}
298export function watchBaseline({ baseline, getPrograms, oldPrograms, sys, oldSnap, baselineSourceMap, baselineDependencies }: WatchBaseline) {
299    if (baselineSourceMap) ts.generateSourceMapBaselineFiles(sys);
300    sys.serializeOutput(baseline);
301    const programs = baselinePrograms(baseline, getPrograms, oldPrograms, baselineDependencies);
302    sys.serializeWatches(baseline);
303    baseline.push(`exitCode:: ExitStatus.${ts.ExitStatus[sys.exitCode as ts.ExitStatus]}`, "");
304    sys.diff(baseline, oldSnap);
305    sys.writtenFiles.forEach((value, key) => {
306        assert.equal(value, 1, `Expected to write file ${key} only once`);
307    });
308    sys.writtenFiles.clear();
309    return programs;
310}
311
312export function baselinePrograms(baseline: string[], getPrograms: () => readonly ts.CommandLineProgram[], oldPrograms: readonly (ts.CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) {
313    const programs = getPrograms();
314    for (let i = 0; i < programs.length; i++) {
315        baselineProgram(baseline, programs[i], oldPrograms[i], baselineDependencies);
316    }
317    return programs;
318}
319
320function baselineProgram(baseline: string[], [program, builderProgram]: ts.CommandLineProgram, oldProgram: ts.CommandLineProgram | undefined, baselineDependencies: boolean | undefined) {
321    if (program !== oldProgram?.[0]) {
322        const options = program.getCompilerOptions();
323        baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`);
324        baseline.push(`Program options: ${JSON.stringify(options)}`);
325        baseline.push(`Program structureReused: ${(ts as any).StructureIsReused[program.structureIsReused]}`);
326        baseline.push("Program files::");
327        for (const file of program.getSourceFiles()) {
328            baseline.push(file.fileName);
329        }
330    }
331    else {
332        baseline.push(`Program: Same as old program`);
333    }
334    baseline.push("");
335
336    if (!builderProgram) return;
337    if (builderProgram !== oldProgram?.[1]) {
338        const state = builderProgram.getState();
339        const internalState = state as unknown as ts.BuilderProgramState;
340        if (state.semanticDiagnosticsPerFile?.size) {
341            baseline.push("Semantic diagnostics in builder refreshed for::");
342            for (const file of program.getSourceFiles()) {
343                if (!internalState.semanticDiagnosticsFromOldState || !internalState.semanticDiagnosticsFromOldState.has(file.resolvedPath)) {
344                    baseline.push(file.fileName);
345                }
346            }
347        }
348        else {
349            baseline.push("No cached semantic diagnostics in the builder::");
350        }
351        if (internalState) {
352            baseline.push("");
353            if (internalState.hasCalledUpdateShapeSignature?.size) {
354                baseline.push("Shape signatures in builder refreshed for::");
355                internalState.hasCalledUpdateShapeSignature.forEach((path: ts.Path) => {
356                    const info = state.fileInfos.get(path);
357                    if (info?.version === info?.signature || !info?.signature) {
358                        baseline.push(path + " (used version)");
359                    }
360                    else if (internalState.filesChangingSignature?.has(path)) {
361                        baseline.push(path + " (computed .d.ts during emit)");
362                    }
363                    else {
364                        baseline.push(path + " (computed .d.ts)");
365                    }
366                });
367            }
368            else {
369                baseline.push("No shapes updated in the builder::");
370            }
371        }
372        baseline.push("");
373        if (!baselineDependencies) return;
374        baseline.push("Dependencies for::");
375        for (const file of builderProgram.getSourceFiles()) {
376            baseline.push(`${file.fileName}:`);
377            for (const depenedency of builderProgram.getAllDependencies(file)) {
378                baseline.push(`  ${depenedency}`);
379            }
380        }
381    }
382    else {
383        baseline.push(`BuilderProgram: Same as old builder program`);
384    }
385    baseline.push("");
386}
387
388export interface VerifyTscWatch extends TscWatchCompile {
389    baselineIncremental?: boolean;
390}
391export function verifyTscWatch(input: VerifyTscWatch) {
392    describe(input.scenario, () => {
393        describe(input.subScenario, () => {
394            tscWatchCompile(input);
395        });
396        if (input.baselineIncremental) {
397            describe(`${input.subScenario} with incremental`, () => {
398                tscWatchCompile({
399                    ...input,
400                    subScenario: `${input.subScenario} with incremental`,
401                    commandLineArgs: [...input.commandLineArgs, "--incremental"],
402                });
403            });
404        }
405    });
406}
407
408export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) {
409    const content = ts.Debug.checkDefined(sys.readFile(file));
410    sys.writeFile(file, content.replace(searchValue, replaceValue));
411}
412
413export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], originalRead?: WatchedSystem["readFile"]) {
414    const host = ts.createSolutionBuilderHostForBaseline(system, /*versionToWrite*/ undefined, originalRead);
415    return ts.createSolutionBuilder(host, rootNames, {});
416}
417
418export function ensureErrorFreeBuild(host: WatchedSystem, rootNames: readonly string[]) {
419    // ts build should succeed
420    solutionBuildWithBaseline(host, rootNames);
421    assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " "));
422}
423
424export function solutionBuildWithBaseline(sys: WatchedSystem, solutionRoots: readonly string[], originalRead?: WatchedSystem["readFile"]) {
425    const originalReadFile = sys.readFile;
426    const originalWrite = sys.write;
427    const originalWriteFile = sys.writeFile;
428    const solutionBuilder = createSolutionBuilder(ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(
429        fakes.patchHostForBuildInfoReadWrite(sys)
430    ), solutionRoots, originalRead);
431    solutionBuilder.build();
432    sys.readFile = originalReadFile;
433    sys.write = originalWrite;
434    sys.writeFile = originalWriteFile;
435    return sys;
436}
437
438export function createSystemWithSolutionBuild(solutionRoots: readonly string[], files: ts.TestFSWithWatch.FileOrFolderOrSymLinkMap | readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[], params?: ts.TestFSWithWatch.TestServerHostCreationParameters) {
439    return solutionBuildWithBaseline(createWatchedSystem(files, params), solutionRoots);
440}
441