Skip to content

Commit 5af7e06

Browse files
authored
Merge pull request microsoft#23972 from Microsoft/batchTestConfigurationsForBrowser
Batch enumerateFiles into a single web request
2 parents 6f9dc2f + 9b04dc3 commit 5af7e06

10 files changed

+284
-82
lines changed

src/harness/compilerRunner.ts

+41-46
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const enum CompilerTestType {
1212
Test262
1313
}
1414

15+
interface CompilerFileBasedTest extends Harness.FileBasedTest {
16+
payload?: Harness.TestCaseParser.TestCaseContent;
17+
}
18+
1519
class CompilerBaselineRunner extends RunnerBase {
1620
private basePath = "tests/cases";
1721
private testSuiteName: TestRunnerKind;
@@ -42,7 +46,8 @@ class CompilerBaselineRunner extends RunnerBase {
4246
}
4347

4448
public enumerateTestFiles() {
45-
return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true });
49+
// see also: `enumerateTestFiles` in tests/webTestServer.ts
50+
return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }).map(CompilerTest.getConfigurations);
4651
}
4752

4853
public initializeTests() {
@@ -52,24 +57,32 @@ class CompilerBaselineRunner extends RunnerBase {
5257
});
5358

5459
// this will set up a series of describe/it blocks to run between the setup and cleanup phases
55-
const files = this.tests.length > 0 ? this.tests : this.enumerateTestFiles();
56-
files.forEach(file => { this.checkTestCodeOutput(vpath.normalizeSeparators(file)); });
60+
const files = this.tests.length > 0 ? this.tests : Harness.IO.enumerateTestFiles(this);
61+
files.forEach(test => {
62+
const file = typeof test === "string" ? test : test.file;
63+
this.checkTestCodeOutput(vpath.normalizeSeparators(file), typeof test === "string" ? CompilerTest.getConfigurations(test) : test);
64+
});
5765
});
5866
}
5967

60-
public checkTestCodeOutput(fileName: string) {
61-
for (const { name, payload } of CompilerTest.getConfigurations(fileName)) {
62-
describe(`${this.testSuiteName} tests for ${fileName}${name ? ` (${name})` : ``}`, () => {
63-
this.runSuite(fileName, payload);
68+
public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) {
69+
if (test && test.configurations) {
70+
test.configurations.forEach(configuration => {
71+
describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${Harness.getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => {
72+
this.runSuite(fileName, test, configuration);
73+
});
6474
});
6575
}
76+
describe(`${this.testSuiteName} tests for ${fileName}}`, () => {
77+
this.runSuite(fileName, test);
78+
});
6679
}
6780

68-
private runSuite(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) {
81+
private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: Harness.FileBasedTestConfiguration) {
6982
// Mocha holds onto the closure environment of the describe callback even after the test is done.
7083
// Everything declared here should be cleared out in the "after" callback.
7184
let compilerTest: CompilerTest | undefined;
72-
before(() => { compilerTest = new CompilerTest(fileName, testCaseContent); });
85+
before(() => { compilerTest = new CompilerTest(fileName, test && test.payload, configuration); });
7386
it(`Correct errors for ${fileName}`, () => { compilerTest.verifyDiagnostics(); });
7487
it(`Correct module resolution tracing for ${fileName}`, () => { compilerTest.verifyModuleResolution(); });
7588
it(`Correct sourcemap content for ${fileName}`, () => { compilerTest.verifySourceMapRecord(); });
@@ -97,11 +110,6 @@ class CompilerBaselineRunner extends RunnerBase {
97110
}
98111
}
99112

100-
interface CompilerTestConfiguration {
101-
name: string;
102-
payload: Harness.TestCaseParser.TestCaseContent;
103-
}
104-
105113
class CompilerTest {
106114
private fileName: string;
107115
private justName: string;
@@ -116,10 +124,20 @@ class CompilerTest {
116124
// equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files)
117125
private otherFiles: Harness.Compiler.TestFile[];
118126

119-
constructor(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) {
127+
constructor(fileName: string, testCaseContent?: Harness.TestCaseParser.TestCaseContent, configurationOverrides?: Harness.TestCaseParser.CompilerSettings) {
120128
this.fileName = fileName;
121129
this.justName = vpath.basename(fileName);
130+
122131
const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/";
132+
133+
if (testCaseContent === undefined) {
134+
testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(Harness.IO.readFile(fileName), fileName, rootDir);
135+
}
136+
137+
if (configurationOverrides) {
138+
testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } };
139+
}
140+
123141
const units = testCaseContent.testUnitData;
124142
this.harnessSettings = testCaseContent.settings;
125143
let tsConfigOptions: ts.CompilerOptions;
@@ -174,32 +192,14 @@ class CompilerTest {
174192
this.options = this.result.options;
175193
}
176194

177-
public static getConfigurations(fileName: string) {
178-
const content = Harness.IO.readFile(fileName);
179-
const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/";
180-
const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, fileName, rootDir);
181-
const configurations: CompilerTestConfiguration[] = [];
182-
const scriptTargets = this._split(testCaseContent.settings.target);
183-
const moduleKinds = this._split(testCaseContent.settings.module);
184-
for (const scriptTarget of scriptTargets) {
185-
for (const moduleKind of moduleKinds) {
186-
let name = "";
187-
if (moduleKinds.length > 1) {
188-
name += `@module: ${moduleKind || "none"}`;
189-
}
190-
if (scriptTargets.length > 1) {
191-
if (name) name += ", ";
192-
name += `@target: ${scriptTarget || "none"}`;
193-
}
194-
195-
const settings = { ...testCaseContent.settings };
196-
if (scriptTarget) settings.target = scriptTarget;
197-
if (moduleKind) settings.module = moduleKind;
198-
configurations.push({ name, payload: { ...testCaseContent, settings } });
199-
}
200-
}
201-
202-
return configurations;
195+
public static getConfigurations(file: string): CompilerFileBasedTest {
196+
// also see `parseCompilerTestConfigurations` in tests/webTestServer.ts
197+
const content = Harness.IO.readFile(file);
198+
const rootDir = file.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(file) + "/";
199+
const payload = Harness.TestCaseParser.makeUnitsFromTest(content, file, rootDir);
200+
const settings = Harness.TestCaseParser.extractCompilerSettings(content);
201+
const configurations = Harness.getFileBasedTestConfigurations(settings, /*varyBy*/ ["module", "target"]);
202+
return { file, payload, configurations };
203203
}
204204

205205
public verifyDiagnostics() {
@@ -267,11 +267,6 @@ class CompilerTest {
267267
this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program.getSourceFile(file.unitName)));
268268
}
269269

270-
private static _split(text: string) {
271-
const entries = text && text.split(",").map(s => s.toLowerCase().trim()).filter(s => s.length > 0);
272-
return entries && entries.length > 0 ? entries : [""];
273-
}
274-
275270
private makeUnitName(name: string, root: string) {
276271
const path = ts.toPath(name, root, ts.identity);
277272
const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", ts.identity);

src/harness/externalCompileRunner.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ abstract class ExternalCompileRunnerBase extends RunnerBase {
2828

2929
describe(`${this.kind()} code samples`, () => {
3030
for (const test of testList) {
31-
this.runTest(test);
31+
this.runTest(typeof test === "string" ? test : test.file);
3232
}
3333
});
3434
}

src/harness/fourslashRunner.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class FourSlashRunner extends RunnerBase {
3636
}
3737

3838
public enumerateTestFiles() {
39+
// see also: `enumerateTestFiles` in tests/webTestServer.ts
3940
return this.enumerateFiles(this.basePath, /\.ts/i, { recursive: false });
4041
}
4142

@@ -45,22 +46,23 @@ class FourSlashRunner extends RunnerBase {
4546

4647
public initializeTests() {
4748
if (this.tests.length === 0) {
48-
this.tests = this.enumerateTestFiles();
49+
this.tests = Harness.IO.enumerateTestFiles(this);
4950
}
5051

5152
describe(this.testSuiteName + " tests", () => {
52-
this.tests.forEach((fn: string) => {
53-
describe(fn, () => {
54-
fn = ts.normalizeSlashes(fn);
55-
const justName = fn.replace(/^.*[\\\/]/, "");
53+
this.tests.forEach(test => {
54+
const file = typeof test === "string" ? test : test.file;
55+
describe(file, () => {
56+
let fn = ts.normalizeSlashes(file);
57+
const justName = fn.replace(/^.*[\\\/]/, "");
5658

57-
// Convert to relative path
58-
const testIndex = fn.indexOf("tests/");
59-
if (testIndex >= 0) fn = fn.substr(testIndex);
59+
// Convert to relative path
60+
const testIndex = fn.indexOf("tests/");
61+
if (testIndex >= 0) fn = fn.substr(testIndex);
6062

61-
if (justName && !justName.match(/fourslash\.ts$/i) && !justName.match(/\.d\.ts$/i)) {
62-
it(this.testSuiteName + " test " + justName + " runs correctly", () => {
63-
FourSlash.runFourSlashTest(this.basePath, this.testType, fn);
63+
if (justName && !justName.match(/fourslash\.ts$/i) && !justName.match(/\.d\.ts$/i)) {
64+
it(this.testSuiteName + " test " + justName + " runs correctly", () => {
65+
FourSlash.runFourSlashTest(this.basePath, this.testType, fn);
6466
});
6567
}
6668
});

src/harness/harness.ts

+82-4
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ namespace Harness {
514514
fileExists(fileName: string): boolean;
515515
directoryExists(path: string): boolean;
516516
deleteFile(fileName: string): void;
517+
enumerateTestFiles(runner: RunnerBase): (string | FileBasedTest)[];
517518
listFiles(path: string, filter?: RegExp, options?: { recursive?: boolean }): string[];
518519
log(text: string): void;
519520
args(): string[];
@@ -559,6 +560,10 @@ namespace Harness {
559560
return dirPath === path ? undefined : dirPath;
560561
}
561562

563+
function enumerateTestFiles(runner: RunnerBase) {
564+
return runner.enumerateTestFiles();
565+
}
566+
562567
function listFiles(path: string, spec: RegExp, options?: { recursive?: boolean }) {
563568
options = options || {};
564569

@@ -639,6 +644,7 @@ namespace Harness {
639644
directoryExists: path => ts.sys.directoryExists(path),
640645
deleteFile,
641646
listFiles,
647+
enumerateTestFiles,
642648
log: s => console.log(s),
643649
args: () => ts.sys.args,
644650
getExecutingFilePath: () => ts.sys.getExecutingFilePath(),
@@ -913,6 +919,11 @@ namespace Harness {
913919
return ts.getDirectoryPath(ts.normalizeSlashes(url.pathname || "/"));
914920
}
915921

922+
function enumerateTestFiles(runner: RunnerBase): (string | FileBasedTest)[] {
923+
const response = send(HttpRequestMessage.post(new URL("/api/enumerateTestFiles", serverRoot), HttpContent.text(runner.kind())));
924+
return hasJsonContent(response) ? JSON.parse(response.content.content) : [];
925+
}
926+
916927
function listFiles(dirname: string, spec?: RegExp, options?: { recursive?: boolean }): string[] {
917928
if (spec || (options && !options.recursive)) {
918929
let results = IO.listFiles(dirname);
@@ -959,6 +970,7 @@ namespace Harness {
959970
directoryExists,
960971
deleteFile,
961972
listFiles: Utils.memoize(listFiles, (path, spec, options) => `${path}|${spec}|${options ? options.recursive === true : true}`),
973+
enumerateTestFiles: Utils.memoize(enumerateTestFiles, runner => runner.kind()),
962974
log: s => console.log(s),
963975
args: () => [],
964976
getExecutingFilePath: () => "",
@@ -1761,6 +1773,74 @@ namespace Harness {
17611773
}
17621774
}
17631775

1776+
export interface FileBasedTest {
1777+
file: string;
1778+
configurations?: FileBasedTestConfiguration[];
1779+
}
1780+
1781+
export interface FileBasedTestConfiguration {
1782+
[key: string]: string;
1783+
}
1784+
1785+
function splitVaryBySettingValue(text: string): string[] | undefined {
1786+
if (!text) return undefined;
1787+
const entries = text.split(/,/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0);
1788+
return entries && entries.length > 1 ? entries : undefined;
1789+
}
1790+
1791+
function computeFileBasedTestConfigurationVariations(configurations: FileBasedTestConfiguration[], variationState: FileBasedTestConfiguration, varyByEntries: [string, string[]][], offset: number) {
1792+
if (offset >= varyByEntries.length) {
1793+
// make a copy of the current variation state
1794+
configurations.push({ ...variationState });
1795+
return;
1796+
}
1797+
1798+
const [varyBy, entries] = varyByEntries[offset];
1799+
for (const entry of entries) {
1800+
// set or overwrite the variation, then compute the next variation
1801+
variationState[varyBy] = entry;
1802+
computeFileBasedTestConfigurationVariations(configurations, variationState, varyByEntries, offset + 1);
1803+
}
1804+
}
1805+
1806+
/**
1807+
* Compute FileBasedTestConfiguration variations based on a supplied list of variable settings.
1808+
*/
1809+
export function getFileBasedTestConfigurations(settings: TestCaseParser.CompilerSettings, varyBy: string[]): FileBasedTestConfiguration[] | undefined {
1810+
let varyByEntries: [string, string[]][] | undefined;
1811+
for (const varyByKey of varyBy) {
1812+
if (ts.hasProperty(settings, varyByKey)) {
1813+
// we only consider variations when there are 2 or more variable entries.
1814+
const entries = splitVaryBySettingValue(settings[varyByKey]);
1815+
if (entries) {
1816+
if (!varyByEntries) varyByEntries = [];
1817+
varyByEntries.push([varyByKey, entries]);
1818+
}
1819+
}
1820+
}
1821+
1822+
if (!varyByEntries) return undefined;
1823+
1824+
const configurations: FileBasedTestConfiguration[] = [];
1825+
computeFileBasedTestConfigurationVariations(configurations, /*variationState*/ {}, varyByEntries, /*offset*/ 0);
1826+
return configurations;
1827+
}
1828+
1829+
/**
1830+
* Compute a description for this configuration based on its entries
1831+
*/
1832+
export function getFileBasedTestConfigurationDescription(configuration: FileBasedTestConfiguration) {
1833+
let name = "";
1834+
if (configuration) {
1835+
const keys = Object.keys(configuration).sort();
1836+
for (const key of keys) {
1837+
if (name) name += ", ";
1838+
name += `@${key}: ${configuration[key]}`;
1839+
}
1840+
}
1841+
return name;
1842+
}
1843+
17641844
export namespace TestCaseParser {
17651845
/** all the necessary information to set the right compiler settings */
17661846
export interface CompilerSettings {
@@ -1779,7 +1859,7 @@ namespace Harness {
17791859
// Regex for parsing options in the format "@Alpha: Value of any sort"
17801860
const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines
17811861

1782-
function extractCompilerSettings(content: string): CompilerSettings {
1862+
export function extractCompilerSettings(content: string): CompilerSettings {
17831863
const opts: CompilerSettings = {};
17841864

17851865
let match: RegExpExecArray;
@@ -1800,9 +1880,7 @@ namespace Harness {
18001880
}
18011881

18021882
/** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */
1803-
export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string): TestCaseContent {
1804-
const settings = extractCompilerSettings(code);
1805-
1883+
export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string, settings = extractCompilerSettings(code)): TestCaseContent {
18061884
// List of all the subfiles we've parsed out
18071885
const testUnitData: TestUnitData[] = [];
18081886

src/harness/parallel/host.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ namespace Harness.Parallel.Host {
8181
const { statSync }: { statSync(path: string): { size: number }; } = require("fs");
8282
const path: { join: (...args: string[]) => string } = require("path");
8383
for (const runner of runners) {
84-
for (const file of runner.enumerateTestFiles()) {
84+
for (const test of runner.enumerateTestFiles()) {
85+
const file = typeof test === "string" ? test : test.file;
8586
let size: number;
8687
if (!perfData) {
8788
try {

src/harness/projectsRunner.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ namespace project {
5151
describe("projects tests", () => {
5252
const tests = this.tests.length === 0 ? this.enumerateTestFiles() : this.tests;
5353
for (const test of tests) {
54-
this.runProjectTestCase(test);
54+
this.runProjectTestCase(typeof test === "string" ? test : test.file);
5555
}
5656
});
5757
}

src/harness/runnerbase.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
/// <reference path="harness.ts" />
2-
3-
41
type TestRunnerKind = CompilerTestKind | FourslashTestKind | "project" | "rwc" | "test262" | "user" | "dt";
52
type CompilerTestKind = "conformance" | "compiler";
63
type FourslashTestKind = "fourslash" | "fourslash-shims" | "fourslash-shims-pp" | "fourslash-server";
74

85
abstract class RunnerBase {
96
// contains the tests to run
10-
public tests: string[] = [];
7+
public tests: (string | Harness.FileBasedTest)[] = [];
118

129
/** Add a source file to the runner's list of tests that need to be initialized with initializeTests */
1310
public addTest(fileName: string) {
@@ -20,7 +17,7 @@ abstract class RunnerBase {
2017

2118
abstract kind(): TestRunnerKind;
2219

23-
abstract enumerateTestFiles(): string[];
20+
abstract enumerateTestFiles(): (string | Harness.FileBasedTest)[];
2421

2522
/** The working directory where tests are found. Needed for batch testing where the input path will differ from the output path inside baselines */
2623
public workingDirectory = "";

0 commit comments

Comments
 (0)