• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as vpath from "./_namespaces/vpath";
2import * as ts from "./_namespaces/ts";
3import * as compiler from "./_namespaces/compiler";
4import * as Utils from "./_namespaces/Utils";
5import {
6    Baseline, Compiler, FileBasedTest, FileBasedTestConfiguration, getFileBasedTestConfigurationDescription,
7    getFileBasedTestConfigurations, IO, RunnerBase, TestCaseParser, TestRunnerKind,
8} from "./_namespaces/Harness";
9
10export const enum CompilerTestType {
11    Conformance,
12    Regressions,
13    Test262,
14    OH
15}
16
17interface CompilerFileBasedTest extends FileBasedTest {
18    readonly content?: string;
19}
20
21export class CompilerBaselineRunner extends RunnerBase {
22    private basePath = "tests/cases";
23    private testSuiteName: TestRunnerKind;
24    private emit: boolean;
25
26    public options: string | undefined;
27
28    constructor(public testType: CompilerTestType) {
29        super();
30        this.emit = true;
31        if (testType === CompilerTestType.Conformance) {
32            this.testSuiteName = "conformance";
33        }
34        else if (testType === CompilerTestType.Regressions) {
35            this.testSuiteName = "compiler";
36        }
37        else if (testType === CompilerTestType.Test262) {
38            this.testSuiteName = "test262";
39        }
40        else if (testType === CompilerTestType.OH) {
41            this.testSuiteName = "compiler-oh";
42        }
43        else {
44            this.testSuiteName = "compiler"; // default to this for historical reasons
45        }
46        this.basePath += "/" + this.testSuiteName;
47    }
48
49    public kind() {
50        return this.testSuiteName;
51    }
52
53    public enumerateTestFiles() {
54        // see also: `enumerateTestFiles` in tests/webTestServer.ts
55        return this.enumerateFiles(this.basePath, /\.(ets|tsx?)$/, { recursive: true }).map(CompilerTest.getConfigurations);
56    }
57
58    public initializeTests() {
59        describe(this.testSuiteName + " tests", () => {
60            describe("Setup compiler for compiler baselines", () => {
61                this.parseOptions();
62            });
63
64            // this will set up a series of describe/it blocks to run between the setup and cleanup phases
65            const files = this.tests.length > 0 ? this.tests : IO.enumerateTestFiles(this);
66            files.forEach(test => {
67                const file = typeof test === "string" ? test : test.file;
68                this.checkTestCodeOutput(vpath.normalizeSeparators(file), typeof test === "string" ? CompilerTest.getConfigurations(test) : test);
69            });
70        });
71    }
72
73    public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) {
74        if (test && ts.some(test.configurations)) {
75            test.configurations.forEach(configuration => {
76                describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => {
77                    this.runSuite(fileName, test, configuration);
78                });
79            });
80        }
81        else {
82            describe(`${this.testSuiteName} tests for ${fileName}`, () => {
83                this.runSuite(fileName, test);
84            });
85        }
86    }
87
88    private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: FileBasedTestConfiguration) {
89        // Mocha holds onto the closure environment of the describe callback even after the test is done.
90        // Everything declared here should be cleared out in the "after" callback.
91        let compilerTest!: CompilerTest;
92        before(() => {
93            let payload;
94            if (test && test.content) {
95                const rootDir = test.file.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(test.file) + "/";
96                payload = TestCaseParser.makeUnitsFromTest(test.content, test.file, rootDir);
97            }
98            compilerTest = new CompilerTest(fileName, payload, configuration);
99        });
100        it(`Correct errors for ${fileName}`, () => compilerTest.verifyDiagnostics());
101        it(`Correct module resolution tracing for ${fileName}`, () => compilerTest.verifyModuleResolution());
102        it(`Correct sourcemap content for ${fileName}`, () => compilerTest.verifySourceMapRecord());
103        it(`Correct JS output for ${fileName}`, () => (this.emit && compilerTest.verifyJavaScriptOutput()));
104        it(`Correct Sourcemap output for ${fileName}`, () => compilerTest.verifySourceMapOutput());
105        it(`Correct type/symbol baselines for ${fileName}`, () => compilerTest.verifyTypesAndSymbols());
106        after(() => {
107            compilerTest = undefined!;
108        });
109    }
110
111    private parseOptions() {
112        if (this.options && this.options.length > 0) {
113            this.emit = false;
114
115            const opts = this.options.split(",");
116            for (const opt of opts) {
117                switch (opt) {
118                    case "emit":
119                        this.emit = true;
120                        break;
121                    default:
122                        throw new Error("unsupported flag");
123                }
124            }
125        }
126    }
127}
128
129class CompilerTest {
130    private static varyBy: readonly string[] = [
131        "module",
132        "moduleResolution",
133        "moduleDetection",
134        "target",
135        "jsx",
136        "removeComments",
137        "importHelpers",
138        "importHelpers",
139        "downlevelIteration",
140        "isolatedModules",
141        "strict",
142        "noImplicitAny",
143        "strictNullChecks",
144        "strictFunctionTypes",
145        "strictBindCallApply",
146        "strictPropertyInitialization",
147        "noImplicitThis",
148        "alwaysStrict",
149        "allowSyntheticDefaultImports",
150        "esModuleInterop",
151        "emitDecoratorMetadata",
152        "skipDefaultLibCheck",
153        "preserveConstEnums",
154        "skipLibCheck",
155        "exactOptionalPropertyTypes",
156        "useDefineForClassFields",
157        "useUnknownInCatchVariables",
158        "noUncheckedIndexedAccess",
159        "noPropertyAccessFromIndexSignature",
160    ];
161    private fileName: string;
162    private justName: string;
163    private configuredName: string;
164    private lastUnit: TestCaseParser.TestUnitData;
165    private harnessSettings: TestCaseParser.CompilerSettings;
166    private hasNonDtsFiles: boolean;
167    private result: compiler.CompilationResult;
168    private options: ts.CompilerOptions;
169    private tsConfigFiles: Compiler.TestFile[];
170    // equivalent to the files that will be passed on the command line
171    private toBeCompiled: Compiler.TestFile[];
172    // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files)
173    private otherFiles: Compiler.TestFile[];
174
175    constructor(fileName: string, testCaseContent?: TestCaseParser.TestCaseContent, configurationOverrides?: TestCaseParser.CompilerSettings) {
176        this.fileName = fileName;
177        this.justName = vpath.basename(fileName);
178        this.configuredName = this.justName;
179        if (configurationOverrides) {
180            let configuredName = "";
181            const keys = Object
182                .keys(configurationOverrides)
183                .sort();
184            for (const key of keys) {
185                if (configuredName) {
186                    configuredName += ",";
187                }
188                configuredName += `${key.toLowerCase()}=${configurationOverrides[key].toLowerCase()}`;
189            }
190            if (configuredName) {
191                const extname = vpath.extname(this.justName);
192                const basename = vpath.basename(this.justName, extname, /*ignoreCase*/ true);
193                this.configuredName = `${basename}(${configuredName})${extname}`;
194            }
195        }
196
197        const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/";
198
199        if (testCaseContent === undefined) {
200            testCaseContent = TestCaseParser.makeUnitsFromTest(IO.readFile(fileName)!, fileName, rootDir);
201        }
202
203        if (configurationOverrides) {
204            testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } };
205        }
206
207        const units = testCaseContent.testUnitData;
208        this.harnessSettings = testCaseContent.settings;
209        let tsConfigOptions: ts.CompilerOptions | undefined;
210        this.tsConfigFiles = [];
211        if (testCaseContent.tsConfig) {
212            assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`);
213            assert.equal(testCaseContent.tsConfig.raw.exclude, undefined, `exclude in tsconfig is not currently supported`);
214
215            tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options);
216            this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData!, rootDir, ts.combinePaths(rootDir, tsConfigOptions.configFilePath)));
217        }
218        else {
219            const baseUrl = this.harnessSettings.baseUrl;
220            if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) {
221                this.harnessSettings.baseUrl = ts.getNormalizedAbsolutePath(baseUrl, rootDir);
222            }
223        }
224
225        this.lastUnit = units[units.length - 1];
226        this.hasNonDtsFiles = units.some(unit => !ts.isDeclarationFileName(unit.name));
227        // We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test)
228        // If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references,
229        // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another.
230        this.toBeCompiled = [];
231        this.otherFiles = [];
232
233        if (testCaseContent.settings.noImplicitReferences || /require\(/.test(this.lastUnit.content) || /reference\spath/.test(this.lastUnit.content)) {
234            this.toBeCompiled.push(this.createHarnessTestFile(this.lastUnit, rootDir));
235            units.forEach(unit => {
236                if (unit.name !== this.lastUnit.name) {
237                    this.otherFiles.push(this.createHarnessTestFile(unit, rootDir));
238                }
239            });
240        }
241        else {
242            this.toBeCompiled = units.map(unit => {
243                return this.createHarnessTestFile(unit, rootDir);
244            });
245        }
246
247        if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) {
248            tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath);
249            tsConfigOptions.configFile!.fileName = tsConfigOptions.configFilePath;
250        }
251
252        this.result = Compiler.compileFiles(
253            this.toBeCompiled,
254            this.otherFiles,
255            this.harnessSettings,
256            /*options*/ tsConfigOptions,
257            /*currentDirectory*/ this.harnessSettings.currentDirectory,
258            testCaseContent.symlinks
259        );
260
261        this.options = this.result.options;
262    }
263
264    public static getConfigurations(file: string): CompilerFileBasedTest {
265        // also see `parseCompilerTestConfigurations` in tests/webTestServer.ts
266        const content = IO.readFile(file)!;
267        const settings = TestCaseParser.extractCompilerSettings(content);
268        const configurations = getFileBasedTestConfigurations(settings, CompilerTest.varyBy);
269        return { file, configurations, content };
270    }
271
272    public verifyDiagnostics() {
273        // check errors
274        Compiler.doErrorBaseline(
275            this.configuredName,
276            this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles),
277            this.result.diagnostics,
278            !!this.options.pretty);
279    }
280
281    public verifyModuleResolution() {
282        if (this.options.traceResolution) {
283            Baseline.runBaseline(this.configuredName.replace(/\.(ets|tsx?)$/, ".trace.json"),
284                JSON.stringify(this.result.traces.map(Utils.sanitizeTraceResolutionLogEntry), undefined, 4));
285        }
286    }
287
288    public verifySourceMapRecord() {
289        if (this.options.sourceMap || this.options.inlineSourceMap || this.options.declarationMap) {
290            const record = Utils.removeTestPathPrefixes(this.result.getSourceMapRecord()!);
291            const baseline = (this.options.noEmitOnError && this.result.diagnostics.length !== 0) || record === undefined
292                // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required.
293                ? null // eslint-disable-line no-null/no-null
294                : record;
295            Baseline.runBaseline(this.configuredName.replace(/\.(ets|tsx?)$/, ".sourcemap.txt"), baseline);
296        }
297    }
298
299    public verifyJavaScriptOutput() {
300        if (this.hasNonDtsFiles) {
301            Compiler.doJsEmitBaseline(
302                this.configuredName,
303                this.fileName,
304                this.options,
305                this.result,
306                this.tsConfigFiles,
307                this.toBeCompiled,
308                this.otherFiles,
309                this.harnessSettings);
310        }
311    }
312
313    public verifySourceMapOutput() {
314        Compiler.doSourcemapBaseline(
315            this.configuredName,
316            this.options,
317            this.result,
318            this.harnessSettings);
319    }
320
321    public verifyTypesAndSymbols() {
322        if (this.fileName.indexOf("APISample") >= 0) {
323            return;
324        }
325
326        const noTypesAndSymbols =
327            this.harnessSettings.noTypesAndSymbols &&
328            this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true";
329        if (noTypesAndSymbols) {
330            return;
331        }
332
333        Compiler.doTypeAndSymbolBaseline(
334            this.configuredName,
335            this.result.program!,
336            this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program!.getSourceFile(file.unitName)),
337            /*opts*/ undefined,
338            /*multifile*/ undefined,
339            /*skipTypeBaselines*/ undefined,
340            /*skipSymbolBaselines*/ undefined,
341            !!ts.length(this.result.diagnostics)
342        );
343    }
344
345    private makeUnitName(name: string, root: string) {
346        const path = ts.toPath(name, root, ts.identity);
347        const pathStart = ts.toPath(IO.getCurrentDirectory(), "", ts.identity);
348        return pathStart ? path.replace(pathStart, "/") : path;
349    }
350
351    private createHarnessTestFile(lastUnit: TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Compiler.TestFile {
352        return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions };
353    }
354}
355