• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.tscWatch {
2    export const projects = `/user/username/projects`;
3    export const projectRoot = `${projects}/myproject`;
4    export import WatchedSystem = TestFSWithWatch.TestServerHost;
5    export type File = TestFSWithWatch.File;
6    export type SymLink = TestFSWithWatch.SymLink;
7    export import libFile = TestFSWithWatch.libFile;
8    export import createWatchedSystem = TestFSWithWatch.createWatchedSystem;
9    export import checkArray = TestFSWithWatch.checkArray;
10    export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles;
11    export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed;
12    export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories;
13    export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed;
14    export import checkOutputContains = TestFSWithWatch.checkOutputContains;
15    export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain;
16
17    export const commonFile1: File = {
18        path: "/a/b/commonFile1.ts",
19        content: "let x = 1"
20    };
21    export const commonFile2: File = {
22        path: "/a/b/commonFile2.ts",
23        content: "let y = 1"
24    };
25
26    export function checkProgramActualFiles(program: Program, expectedFiles: readonly string[]) {
27        checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles);
28    }
29
30    export function checkProgramRootFiles(program: Program, expectedFiles: readonly string[]) {
31        checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles);
32    }
33
34    export type Watch = WatchOfConfigFile<EmitAndSemanticDiagnosticsBuilderProgram> | WatchOfFilesAndCompilerOptions<EmitAndSemanticDiagnosticsBuilderProgram>;
35
36    export function createWatchOfConfigFile(configFileName: string, system: WatchedSystem, optionsToExtend?: CompilerOptions, watchOptionsToExtend?: WatchOptions) {
37        const compilerHost = createWatchCompilerHostOfConfigFile({ configFileName, optionsToExtend, watchOptionsToExtend, system });
38        return createWatchProgram(compilerHost);
39    }
40
41    export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], system: WatchedSystem, options: CompilerOptions = {}, watchOptions?: WatchOptions) {
42        const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions({ rootFiles, options, watchOptions, system });
43        return createWatchProgram(compilerHost);
44    }
45
46    const elapsedRegex = /^Elapsed:: \d+(?:\.\d+)?ms/;
47    const buildVerboseLogRegEx = /^.+ \- /;
48    export enum HostOutputKind {
49        Log,
50        Diagnostic,
51        WatchDiagnostic
52    }
53
54    export interface HostOutputLog {
55        kind: HostOutputKind.Log;
56        expected: string;
57        caption?: string;
58    }
59
60    export interface HostOutputDiagnostic {
61        kind: HostOutputKind.Diagnostic;
62        diagnostic: Diagnostic | string;
63    }
64
65    export interface HostOutputWatchDiagnostic {
66        kind: HostOutputKind.WatchDiagnostic;
67        diagnostic: Diagnostic | string;
68    }
69
70    export type HostOutput = HostOutputLog | HostOutputDiagnostic | HostOutputWatchDiagnostic;
71
72    export function checkOutputErrors(
73        host: WatchedSystem,
74        expected: readonly HostOutput[],
75        disableConsoleClears?: boolean | undefined
76    ) {
77        let screenClears = 0;
78        const outputs = host.getOutput();
79        assert.equal(outputs.length, expected.length, JSON.stringify(outputs));
80        let index = 0;
81        forEach(expected, expected => {
82            switch (expected.kind) {
83                case HostOutputKind.Log:
84                    return assertLog(expected);
85                case HostOutputKind.Diagnostic:
86                    return assertDiagnostic(expected);
87                case HostOutputKind.WatchDiagnostic:
88                    return assertWatchDiagnostic(expected);
89                default:
90                    return Debug.assertNever(expected);
91            }
92        });
93        assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears");
94        host.clearOutput();
95
96        function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic {
97            return !!(diagnostic as Diagnostic).messageText;
98        }
99
100        function assertDiagnostic({ diagnostic }: HostOutputDiagnostic) {
101            const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic;
102            assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected));
103            index++;
104        }
105
106        function getCleanLogString(log: string) {
107            return log.replace(elapsedRegex, "").replace(buildVerboseLogRegEx, "");
108        }
109
110        function assertLog({ caption, expected }: HostOutputLog) {
111            const actual = outputs[index];
112            assert.equal(getCleanLogString(actual), getCleanLogString(expected), getOutputAtFailedMessage(caption || "Log", expected));
113            index++;
114        }
115
116        function assertWatchDiagnostic({ diagnostic }: HostOutputWatchDiagnostic) {
117            if (isString(diagnostic)) {
118                assert.equal(outputs[index], diagnostic, getOutputAtFailedMessage("Diagnostic", diagnostic));
119            }
120            else {
121                const expected = getWatchDiagnosticWithoutDate(diagnostic);
122                if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) {
123                    assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`);
124                    screenClears++;
125                }
126                assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected));
127            }
128            index++;
129        }
130
131        function getOutputAtFailedMessage(caption: string, expectedOutput: string) {
132            return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`;
133        }
134
135        function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) {
136            const newLines = contains(screenStartingMessageCodes, diagnostic.code)
137                ? `${host.newLine}${host.newLine}`
138                : host.newLine;
139            return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`;
140        }
141    }
142
143    export function hostOutputLog(expected: string, caption?: string): HostOutputLog {
144        return { kind: HostOutputKind.Log, expected, caption };
145    }
146    export function hostOutputDiagnostic(diagnostic: Diagnostic | string): HostOutputDiagnostic {
147        return { kind: HostOutputKind.Diagnostic, diagnostic };
148    }
149    export function hostOutputWatchDiagnostic(diagnostic: Diagnostic | string): HostOutputWatchDiagnostic {
150        return { kind: HostOutputKind.WatchDiagnostic, diagnostic };
151    }
152
153    export function startingCompilationInWatchMode() {
154        return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode));
155    }
156    export function foundErrorsWatching(errors: readonly any[]) {
157        return hostOutputWatchDiagnostic(errors.length === 1 ?
158            createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) :
159            createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length)
160        );
161    }
162    export function fileChangeDetected() {
163        return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation));
164    }
165
166    export function checkOutputErrorsInitial(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeErrors?: string[]) {
167        checkOutputErrors(
168            host,
169            [
170                startingCompilationInWatchMode(),
171                ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")),
172                ...map(errors, hostOutputDiagnostic),
173                foundErrorsWatching(errors)
174            ],
175            disableConsoleClears
176        );
177    }
178
179    export function checkOutputErrorsIncremental(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) {
180        checkOutputErrors(
181            host,
182            [
183                ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")),
184                fileChangeDetected(),
185                ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")),
186                ...map(errors, hostOutputDiagnostic),
187                foundErrorsWatching(errors)
188            ],
189            disableConsoleClears
190        );
191    }
192
193    export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) {
194        checkOutputErrors(
195            host,
196            [
197                ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")),
198                fileChangeDetected(),
199                ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")),
200                ...map(errors, hostOutputDiagnostic),
201            ],
202            disableConsoleClears
203        );
204        assert.equal(host.exitCode, expectedExitCode);
205    }
206
207    export function checkNormalBuildErrors(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], reportErrorSummary?: boolean) {
208        checkOutputErrors(
209            host,
210            [
211                ...map(errors, hostOutputDiagnostic),
212                ...reportErrorSummary ?
213                    [hostOutputWatchDiagnostic(getErrorSummaryText(errors.length, host.newLine))] :
214                    emptyArray
215            ]
216        );
217    }
218
219    export function getDiagnosticMessageChain(message: DiagnosticMessage, args?: (string | number)[], next?: DiagnosticMessageChain[]): DiagnosticMessageChain {
220        let text = getLocaleSpecificMessage(message);
221        if (args?.length) {
222            text = formatStringFromArgs(text, args);
223        }
224        return {
225            messageText: text,
226            category: message.category,
227            code: message.code,
228            next
229        };
230    }
231
232    function isDiagnosticMessageChain(message: DiagnosticMessage | DiagnosticMessageChain): message is DiagnosticMessageChain {
233        return !!(message as DiagnosticMessageChain).messageText;
234    }
235
236    export function getDiagnosticOfFileFrom(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic {
237        return {
238            file,
239            start,
240            length,
241
242            messageText: isDiagnosticMessageChain(message) ?
243                message :
244                getDiagnosticMessageChain(message, args).messageText,
245            category: message.category,
246            code: message.code,
247        };
248    }
249
250    export function getDiagnosticWithoutFile(message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic {
251        return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args);
252    }
253
254    export function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic {
255        return getDiagnosticOfFileFrom(file, start, length, message, ...args);
256    }
257
258    export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic {
259        return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())),
260            start, length, message, ...args);
261    }
262
263    export function getUnknownCompilerOption(program: Program, configFile: File, option: string) {
264        const quotedOption = `"${option}"`;
265        return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option);
266    }
267
268    export function getUnknownDidYouMeanCompilerOption(program: Program, configFile: File, option: string, didYouMean: string) {
269        const quotedOption = `"${option}"`;
270        return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean);
271    }
272
273    export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) {
274        const quotedModuleName = `"${moduleName}"`;
275        return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option, moduleName);
276    }
277
278    export function runQueuedTimeoutCallbacks(sys: WatchedSystem) {
279        sys.runQueuedTimeoutCallbacks();
280    }
281
282    export function checkSingleTimeoutQueueLengthAndRun(sys: WatchedSystem) {
283        sys.checkTimeoutQueueLengthAndRun(1);
284    }
285
286    export function checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys: WatchedSystem) {
287        sys.checkTimeoutQueueLengthAndRun(1);
288        sys.checkTimeoutQueueLength(0);
289    }
290
291    export interface TscWatchCompileChange {
292        caption: string;
293        change: (sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void;
294        timeouts: (
295            sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles,
296            programs: readonly CommandLineProgram[],
297            watchOrSolution: ReturnType<typeof executeCommandLine>
298        ) => void;
299    }
300    export interface TscWatchCheckOptions {
301        baselineSourceMap?: boolean;
302    }
303    export interface TscWatchCompileBase extends TscWatchCheckOptions {
304        scenario: string;
305        subScenario: string;
306        commandLineArgs: readonly string[];
307        changes: readonly TscWatchCompileChange[];
308    }
309    export interface TscWatchCompile extends TscWatchCompileBase {
310        sys: () => WatchedSystem;
311    }
312
313    export const noopChange: TscWatchCompileChange = {
314        caption: "No change",
315        change: noop,
316        timeouts: sys => sys.checkTimeoutQueueLength(0),
317    };
318
319    export type SystemSnap = ReturnType<WatchedSystem["snap"]>;
320    function tscWatchCompile(input: TscWatchCompile) {
321        it("tsc-watch:: Generates files matching the baseline", () => {
322            const { sys, baseline, oldSnap } = createBaseline(input.sys());
323            const {
324                scenario, subScenario,
325                commandLineArgs, changes,
326                baselineSourceMap
327            } = input;
328
329            if (!isWatch(commandLineArgs)) sys.exit = exitCode => sys.exitCode = exitCode;
330            const { cb, getPrograms } = commandLineCallbacks(sys);
331            const watchOrSolution = executeCommandLine(
332                sys,
333                cb,
334                commandLineArgs,
335            );
336            runWatchBaseline({
337                scenario,
338                subScenario,
339                commandLineArgs,
340                sys,
341                baseline,
342                oldSnap,
343                getPrograms,
344                baselineSourceMap,
345                changes,
346                watchOrSolution
347            });
348        });
349    }
350
351    export interface Baseline {
352        baseline: string[];
353        sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles;
354        oldSnap: SystemSnap;
355    }
356
357    export function createBaseline(system: WatchedSystem): Baseline {
358        const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles(
359            fakes.patchHostForBuildInfoReadWrite(system)
360        );
361        const baseline: string[] = [];
362        baseline.push("Input::");
363        sys.diff(baseline);
364        return { sys, baseline, oldSnap: sys.snap() };
365    }
366
367    export function applyChange(sys: Baseline["sys"], baseline: Baseline["baseline"], change: TscWatchCompileChange["change"], caption?: TscWatchCompileChange["caption"]) {
368        const oldSnap = sys.snap();
369        baseline.push(`Change::${caption ? " " + caption : ""}`, "");
370        change(sys);
371        baseline.push("Input::");
372        sys.diff(baseline, oldSnap);
373        return sys.snap();
374    }
375
376    export interface RunWatchBaseline extends Baseline, TscWatchCompileBase {
377        sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles;
378        getPrograms: () => readonly CommandLineProgram[];
379        watchOrSolution: ReturnType<typeof executeCommandLine>;
380    }
381    export function runWatchBaseline({
382        scenario, subScenario, commandLineArgs,
383        getPrograms, sys, baseline, oldSnap,
384        baselineSourceMap,
385        changes, watchOrSolution
386    }: RunWatchBaseline) {
387        baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`);
388        let programs = watchBaseline({
389            baseline,
390            getPrograms,
391            sys,
392            oldSnap,
393            baselineSourceMap
394        });
395
396        for (const { caption, change, timeouts } of changes) {
397            oldSnap = applyChange(sys, baseline, change, caption);
398            timeouts(sys, programs, watchOrSolution);
399            programs = watchBaseline({
400                baseline,
401                getPrograms,
402                sys,
403                oldSnap,
404                baselineSourceMap
405            });
406        }
407        Harness.Baseline.runBaseline(`${isBuild(commandLineArgs) ?
408            isWatch(commandLineArgs) ? "tsbuild/watchMode" : "tsbuild" :
409            isWatch(commandLineArgs) ? "tscWatch" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n"));
410    }
411
412    function isWatch(commandLineArgs: readonly string[]) {
413        return forEach(commandLineArgs, arg => {
414            if (arg.charCodeAt(0) !== CharacterCodes.minus) return false;
415            const option = arg.slice(arg.charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase();
416            return option === "watch" || option === "w";
417        });
418    }
419
420    export interface WatchBaseline extends Baseline, TscWatchCheckOptions {
421        getPrograms: () => readonly CommandLineProgram[];
422    }
423    export function watchBaseline({ baseline, getPrograms, sys, oldSnap, baselineSourceMap }: WatchBaseline) {
424        if (baselineSourceMap) generateSourceMapBaselineFiles(sys);
425        sys.serializeOutput(baseline);
426        const programs = baselinePrograms(baseline, getPrograms);
427        sys.serializeWatches(baseline);
428        baseline.push(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}`, "");
429        sys.diff(baseline, oldSnap);
430        sys.writtenFiles.forEach((value, key) => {
431            assert.equal(value, 1, `Expected to write file ${key} only once`);
432        });
433        sys.writtenFiles.clear();
434        return programs;
435    }
436
437    export function baselinePrograms(baseline: string[], getPrograms: () => readonly CommandLineProgram[]) {
438        const programs = getPrograms();
439        for (const program of programs) {
440            baselineProgram(baseline, program);
441        }
442        return programs;
443    }
444
445    function baselineProgram(baseline: string[], [program, builderProgram]: CommandLineProgram) {
446        const options = program.getCompilerOptions();
447        baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`);
448        baseline.push(`Program options: ${JSON.stringify(options)}`);
449        baseline.push(`Program structureReused: ${(<any>ts).StructureIsReused[program.structureIsReused]}`);
450        baseline.push("Program files::");
451        for (const file of program.getSourceFiles()) {
452            baseline.push(file.fileName);
453        }
454        baseline.push("");
455        if (!builderProgram) return;
456        const state = builderProgram.getState();
457        if (state.semanticDiagnosticsPerFile?.size) {
458            baseline.push("Semantic diagnostics in builder refreshed for::");
459            for (const file of program.getSourceFiles()) {
460                if (!state.semanticDiagnosticsFromOldState || !state.semanticDiagnosticsFromOldState.has(file.resolvedPath)) {
461                    baseline.push(file.fileName);
462                }
463            }
464        }
465        else {
466            baseline.push("No cached semantic diagnostics in the builder::");
467        }
468        baseline.push("");
469    }
470
471    export interface VerifyTscWatch extends TscWatchCompile {
472        baselineIncremental?: boolean;
473    }
474    export function verifyTscWatch(input: VerifyTscWatch) {
475        describe(input.scenario, () => {
476            describe(input.subScenario, () => {
477                tscWatchCompile(input);
478            });
479            if (input.baselineIncremental) {
480                describe(`${input.subScenario} with incremental`, () => {
481                    tscWatchCompile({
482                        ...input,
483                        subScenario: `${input.subScenario} with incremental`,
484                        commandLineArgs: [...input.commandLineArgs, "--incremental"],
485                    });
486                });
487            }
488        });
489    }
490
491    export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) {
492        const content = Debug.checkDefined(sys.readFile(file));
493        sys.writeFile(file, content.replace(searchValue, replaceValue));
494    }
495}
496