diff --git a/src/harness/compilerRunner.ts b/src/harness/compilerRunner.ts index 1ffa2fc63a7b7..91a95e22c89d2 100644 --- a/src/harness/compilerRunner.ts +++ b/src/harness/compilerRunner.ts @@ -12,6 +12,10 @@ const enum CompilerTestType { Test262 } +interface CompilerFileBasedTest extends Harness.FileBasedTest { + payload?: Harness.TestCaseParser.TestCaseContent; +} + class CompilerBaselineRunner extends RunnerBase { private basePath = "tests/cases"; private testSuiteName: TestRunnerKind; @@ -42,7 +46,8 @@ class CompilerBaselineRunner extends RunnerBase { } public enumerateTestFiles() { - return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }); + // see also: `enumerateTestFiles` in tests/webTestServer.ts + return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }).map(CompilerTest.getConfigurations); } public initializeTests() { @@ -52,24 +57,32 @@ class CompilerBaselineRunner extends RunnerBase { }); // this will set up a series of describe/it blocks to run between the setup and cleanup phases - const files = this.tests.length > 0 ? this.tests : this.enumerateTestFiles(); - files.forEach(file => { this.checkTestCodeOutput(vpath.normalizeSeparators(file)); }); + const files = this.tests.length > 0 ? this.tests : Harness.IO.enumerateTestFiles(this); + files.forEach(test => { + const file = typeof test === "string" ? test : test.file; + this.checkTestCodeOutput(vpath.normalizeSeparators(file), typeof test === "string" ? CompilerTest.getConfigurations(test) : test); + }); }); } - public checkTestCodeOutput(fileName: string) { - for (const { name, payload } of CompilerTest.getConfigurations(fileName)) { - describe(`${this.testSuiteName} tests for ${fileName}${name ? ` (${name})` : ``}`, () => { - this.runSuite(fileName, payload); + public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) { + if (test && test.configurations) { + test.configurations.forEach(configuration => { + describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${Harness.getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => { + this.runSuite(fileName, test, configuration); + }); }); } + describe(`${this.testSuiteName} tests for ${fileName}}`, () => { + this.runSuite(fileName, test); + }); } - private runSuite(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) { + private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: Harness.FileBasedTestConfiguration) { // Mocha holds onto the closure environment of the describe callback even after the test is done. // Everything declared here should be cleared out in the "after" callback. let compilerTest: CompilerTest | undefined; - before(() => { compilerTest = new CompilerTest(fileName, testCaseContent); }); + before(() => { compilerTest = new CompilerTest(fileName, test && test.payload, configuration); }); it(`Correct errors for ${fileName}`, () => { compilerTest.verifyDiagnostics(); }); it(`Correct module resolution tracing for ${fileName}`, () => { compilerTest.verifyModuleResolution(); }); it(`Correct sourcemap content for ${fileName}`, () => { compilerTest.verifySourceMapRecord(); }); @@ -97,11 +110,6 @@ class CompilerBaselineRunner extends RunnerBase { } } -interface CompilerTestConfiguration { - name: string; - payload: Harness.TestCaseParser.TestCaseContent; -} - class CompilerTest { private fileName: string; private justName: string; @@ -116,10 +124,20 @@ class CompilerTest { // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) private otherFiles: Harness.Compiler.TestFile[]; - constructor(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) { + constructor(fileName: string, testCaseContent?: Harness.TestCaseParser.TestCaseContent, configurationOverrides?: Harness.TestCaseParser.CompilerSettings) { this.fileName = fileName; this.justName = vpath.basename(fileName); + const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; + + if (testCaseContent === undefined) { + testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(Harness.IO.readFile(fileName), fileName, rootDir); + } + + if (configurationOverrides) { + testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } }; + } + const units = testCaseContent.testUnitData; this.harnessSettings = testCaseContent.settings; let tsConfigOptions: ts.CompilerOptions; @@ -174,32 +192,14 @@ class CompilerTest { this.options = this.result.options; } - public static getConfigurations(fileName: string) { - const content = Harness.IO.readFile(fileName); - const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; - const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, fileName, rootDir); - const configurations: CompilerTestConfiguration[] = []; - const scriptTargets = this._split(testCaseContent.settings.target); - const moduleKinds = this._split(testCaseContent.settings.module); - for (const scriptTarget of scriptTargets) { - for (const moduleKind of moduleKinds) { - let name = ""; - if (moduleKinds.length > 1) { - name += `@module: ${moduleKind || "none"}`; - } - if (scriptTargets.length > 1) { - if (name) name += ", "; - name += `@target: ${scriptTarget || "none"}`; - } - - const settings = { ...testCaseContent.settings }; - if (scriptTarget) settings.target = scriptTarget; - if (moduleKind) settings.module = moduleKind; - configurations.push({ name, payload: { ...testCaseContent, settings } }); - } - } - - return configurations; + public static getConfigurations(file: string): CompilerFileBasedTest { + // also see `parseCompilerTestConfigurations` in tests/webTestServer.ts + const content = Harness.IO.readFile(file); + const rootDir = file.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(file) + "/"; + const payload = Harness.TestCaseParser.makeUnitsFromTest(content, file, rootDir); + const settings = Harness.TestCaseParser.extractCompilerSettings(content); + const configurations = Harness.getFileBasedTestConfigurations(settings, /*varyBy*/ ["module", "target"]); + return { file, payload, configurations }; } public verifyDiagnostics() { @@ -267,11 +267,6 @@ class CompilerTest { this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program.getSourceFile(file.unitName))); } - private static _split(text: string) { - const entries = text && text.split(",").map(s => s.toLowerCase().trim()).filter(s => s.length > 0); - return entries && entries.length > 0 ? entries : [""]; - } - private makeUnitName(name: string, root: string) { const path = ts.toPath(name, root, ts.identity); const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", ts.identity); diff --git a/src/harness/externalCompileRunner.ts b/src/harness/externalCompileRunner.ts index 1d45351114d1b..c60fc4526acb6 100644 --- a/src/harness/externalCompileRunner.ts +++ b/src/harness/externalCompileRunner.ts @@ -28,7 +28,7 @@ abstract class ExternalCompileRunnerBase extends RunnerBase { describe(`${this.kind()} code samples`, () => { for (const test of testList) { - this.runTest(test); + this.runTest(typeof test === "string" ? test : test.file); } }); } diff --git a/src/harness/fourslashRunner.ts b/src/harness/fourslashRunner.ts index d835054a868c7..d0ad139205ab0 100644 --- a/src/harness/fourslashRunner.ts +++ b/src/harness/fourslashRunner.ts @@ -36,6 +36,7 @@ class FourSlashRunner extends RunnerBase { } public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts return this.enumerateFiles(this.basePath, /\.ts/i, { recursive: false }); } @@ -45,22 +46,23 @@ class FourSlashRunner extends RunnerBase { public initializeTests() { if (this.tests.length === 0) { - this.tests = this.enumerateTestFiles(); + this.tests = Harness.IO.enumerateTestFiles(this); } describe(this.testSuiteName + " tests", () => { - this.tests.forEach((fn: string) => { - describe(fn, () => { - fn = ts.normalizeSlashes(fn); - const justName = fn.replace(/^.*[\\\/]/, ""); + this.tests.forEach(test => { + const file = typeof test === "string" ? test : test.file; + describe(file, () => { + let fn = ts.normalizeSlashes(file); + const justName = fn.replace(/^.*[\\\/]/, ""); - // Convert to relative path - const testIndex = fn.indexOf("tests/"); - if (testIndex >= 0) fn = fn.substr(testIndex); + // Convert to relative path + const testIndex = fn.indexOf("tests/"); + if (testIndex >= 0) fn = fn.substr(testIndex); - if (justName && !justName.match(/fourslash\.ts$/i) && !justName.match(/\.d\.ts$/i)) { - it(this.testSuiteName + " test " + justName + " runs correctly", () => { - FourSlash.runFourSlashTest(this.basePath, this.testType, fn); + if (justName && !justName.match(/fourslash\.ts$/i) && !justName.match(/\.d\.ts$/i)) { + it(this.testSuiteName + " test " + justName + " runs correctly", () => { + FourSlash.runFourSlashTest(this.basePath, this.testType, fn); }); } }); diff --git a/src/harness/harness.ts b/src/harness/harness.ts index d73bfd2ed63f1..28233b797da47 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -514,6 +514,7 @@ namespace Harness { fileExists(fileName: string): boolean; directoryExists(path: string): boolean; deleteFile(fileName: string): void; + enumerateTestFiles(runner: RunnerBase): (string | FileBasedTest)[]; listFiles(path: string, filter?: RegExp, options?: { recursive?: boolean }): string[]; log(text: string): void; args(): string[]; @@ -559,6 +560,10 @@ namespace Harness { return dirPath === path ? undefined : dirPath; } + function enumerateTestFiles(runner: RunnerBase) { + return runner.enumerateTestFiles(); + } + function listFiles(path: string, spec: RegExp, options?: { recursive?: boolean }) { options = options || {}; @@ -639,6 +644,7 @@ namespace Harness { directoryExists: path => ts.sys.directoryExists(path), deleteFile, listFiles, + enumerateTestFiles, log: s => console.log(s), args: () => ts.sys.args, getExecutingFilePath: () => ts.sys.getExecutingFilePath(), @@ -913,6 +919,11 @@ namespace Harness { return ts.getDirectoryPath(ts.normalizeSlashes(url.pathname || "/")); } + function enumerateTestFiles(runner: RunnerBase): (string | FileBasedTest)[] { + const response = send(HttpRequestMessage.post(new URL("/api/enumerateTestFiles", serverRoot), HttpContent.text(runner.kind()))); + return hasJsonContent(response) ? JSON.parse(response.content.content) : []; + } + function listFiles(dirname: string, spec?: RegExp, options?: { recursive?: boolean }): string[] { if (spec || (options && !options.recursive)) { let results = IO.listFiles(dirname); @@ -959,6 +970,7 @@ namespace Harness { directoryExists, deleteFile, listFiles: Utils.memoize(listFiles, (path, spec, options) => `${path}|${spec}|${options ? options.recursive === true : true}`), + enumerateTestFiles: Utils.memoize(enumerateTestFiles, runner => runner.kind()), log: s => console.log(s), args: () => [], getExecutingFilePath: () => "", @@ -1761,6 +1773,74 @@ namespace Harness { } } + export interface FileBasedTest { + file: string; + configurations?: FileBasedTestConfiguration[]; + } + + export interface FileBasedTestConfiguration { + [key: string]: string; + } + + function splitVaryBySettingValue(text: string): string[] | undefined { + if (!text) return undefined; + const entries = text.split(/,/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0); + return entries && entries.length > 1 ? entries : undefined; + } + + function computeFileBasedTestConfigurationVariations(configurations: FileBasedTestConfiguration[], variationState: FileBasedTestConfiguration, varyByEntries: [string, string[]][], offset: number) { + if (offset >= varyByEntries.length) { + // make a copy of the current variation state + configurations.push({ ...variationState }); + return; + } + + const [varyBy, entries] = varyByEntries[offset]; + for (const entry of entries) { + // set or overwrite the variation, then compute the next variation + variationState[varyBy] = entry; + computeFileBasedTestConfigurationVariations(configurations, variationState, varyByEntries, offset + 1); + } + } + + /** + * Compute FileBasedTestConfiguration variations based on a supplied list of variable settings. + */ + export function getFileBasedTestConfigurations(settings: TestCaseParser.CompilerSettings, varyBy: string[]): FileBasedTestConfiguration[] | undefined { + let varyByEntries: [string, string[]][] | undefined; + for (const varyByKey of varyBy) { + if (ts.hasProperty(settings, varyByKey)) { + // we only consider variations when there are 2 or more variable entries. + const entries = splitVaryBySettingValue(settings[varyByKey]); + if (entries) { + if (!varyByEntries) varyByEntries = []; + varyByEntries.push([varyByKey, entries]); + } + } + } + + if (!varyByEntries) return undefined; + + const configurations: FileBasedTestConfiguration[] = []; + computeFileBasedTestConfigurationVariations(configurations, /*variationState*/ {}, varyByEntries, /*offset*/ 0); + return configurations; + } + + /** + * Compute a description for this configuration based on its entries + */ + export function getFileBasedTestConfigurationDescription(configuration: FileBasedTestConfiguration) { + let name = ""; + if (configuration) { + const keys = Object.keys(configuration).sort(); + for (const key of keys) { + if (name) name += ", "; + name += `@${key}: ${configuration[key]}`; + } + } + return name; + } + export namespace TestCaseParser { /** all the necessary information to set the right compiler settings */ export interface CompilerSettings { @@ -1779,7 +1859,7 @@ namespace Harness { // Regex for parsing options in the format "@Alpha: Value of any sort" const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines - function extractCompilerSettings(content: string): CompilerSettings { + export function extractCompilerSettings(content: string): CompilerSettings { const opts: CompilerSettings = {}; let match: RegExpExecArray; @@ -1800,9 +1880,7 @@ namespace Harness { } /** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */ - export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string): TestCaseContent { - const settings = extractCompilerSettings(code); - + export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string, settings = extractCompilerSettings(code)): TestCaseContent { // List of all the subfiles we've parsed out const testUnitData: TestUnitData[] = []; diff --git a/src/harness/parallel/host.ts b/src/harness/parallel/host.ts index 75712bc7d9b30..47c5216e16eda 100644 --- a/src/harness/parallel/host.ts +++ b/src/harness/parallel/host.ts @@ -81,7 +81,8 @@ namespace Harness.Parallel.Host { const { statSync }: { statSync(path: string): { size: number }; } = require("fs"); const path: { join: (...args: string[]) => string } = require("path"); for (const runner of runners) { - for (const file of runner.enumerateTestFiles()) { + for (const test of runner.enumerateTestFiles()) { + const file = typeof test === "string" ? test : test.file; let size: number; if (!perfData) { try { diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 8c396ec44f11a..08e92a73be235 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -51,7 +51,7 @@ namespace project { describe("projects tests", () => { const tests = this.tests.length === 0 ? this.enumerateTestFiles() : this.tests; for (const test of tests) { - this.runProjectTestCase(test); + this.runProjectTestCase(typeof test === "string" ? test : test.file); } }); } diff --git a/src/harness/runnerbase.ts b/src/harness/runnerbase.ts index 80532d30b6f09..b29ebb7f92009 100644 --- a/src/harness/runnerbase.ts +++ b/src/harness/runnerbase.ts @@ -1,13 +1,10 @@ -/// - - type TestRunnerKind = CompilerTestKind | FourslashTestKind | "project" | "rwc" | "test262" | "user" | "dt"; type CompilerTestKind = "conformance" | "compiler"; type FourslashTestKind = "fourslash" | "fourslash-shims" | "fourslash-shims-pp" | "fourslash-server"; abstract class RunnerBase { // contains the tests to run - public tests: string[] = []; + public tests: (string | Harness.FileBasedTest)[] = []; /** Add a source file to the runner's list of tests that need to be initialized with initializeTests */ public addTest(fileName: string) { @@ -20,7 +17,7 @@ abstract class RunnerBase { abstract kind(): TestRunnerKind; - abstract enumerateTestFiles(): string[]; + abstract enumerateTestFiles(): (string | Harness.FileBasedTest)[]; /** The working directory where tests are found. Needed for batch testing where the input path will differ from the output path inside baselines */ public workingDirectory = ""; diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts index bfd0153466cea..d2adc112df8ff 100644 --- a/src/harness/rwcRunner.ts +++ b/src/harness/rwcRunner.ts @@ -232,6 +232,7 @@ namespace RWC { class RWCRunner extends RunnerBase { public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts return Harness.IO.getDirectories("internal/cases/rwc/"); } @@ -245,7 +246,7 @@ class RWCRunner extends RunnerBase { public initializeTests(): void { // Read in and evaluate the test list for (const test of this.tests && this.tests.length ? this.tests : this.enumerateTestFiles()) { - this.runTest(test); + this.runTest(typeof test === "string" ? test : test.file); } } diff --git a/src/harness/test262Runner.ts b/src/harness/test262Runner.ts index 08a42ed2ff739..7f83d9101bdd3 100644 --- a/src/harness/test262Runner.ts +++ b/src/harness/test262Runner.ts @@ -101,6 +101,7 @@ class Test262BaselineRunner extends RunnerBase { } public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts return ts.map(this.enumerateFiles(Test262BaselineRunner.basePath, Test262BaselineRunner.testFileExtensionRegex, { recursive: true }), ts.normalizePath); } @@ -113,7 +114,7 @@ class Test262BaselineRunner extends RunnerBase { }); } else { - this.tests.forEach(test => this.runTest(test)); + this.tests.forEach(test => this.runTest(typeof test === "string" ? test : test.file)); } } } diff --git a/tests/webTestServer.ts b/tests/webTestServer.ts index 5309d58f768a9..22f066bbf85ab 100644 --- a/tests/webTestServer.ts +++ b/tests/webTestServer.ts @@ -24,6 +24,15 @@ let browser = "IE"; let grep: string | undefined; let verbose = false; +interface FileBasedTest { + file: string; + configurations?: FileBasedTestConfiguration[]; +} + +interface FileBasedTestConfiguration { + [setting: string]: string; +} + function isFileSystemCaseSensitive(): boolean { // win32\win64 are case insensitive platforms const platform = os.platform(); @@ -84,6 +93,20 @@ function toClientPath(pathname: string) { return clientPath; } +function flatMap(array: T[], selector: (value: T) => U | U[]) { + let result: U[] = []; + for (const item of array) { + const mapped = selector(item); + if (Array.isArray(mapped)) { + result = result.concat(mapped); + } + else { + result.push(mapped); + } + } + return result; +} + declare module "http" { interface IncomingHttpHeaders { "if-match"?: string; @@ -640,24 +663,107 @@ function handleApiResolve(req: http.ServerRequest, res: http.ServerResponse) { }); } +function handleApiEnumerateTestFiles(req: http.ServerRequest, res: http.ServerResponse) { + readContent(req, (err, content) => { + try { + if (err) return sendInternalServerError(res, err); + if (!content) return sendBadRequest(res); + const tests: (string | FileBasedTest)[] = enumerateTestFiles(content); + return sendJson(res, /*statusCode*/ 200, tests); + } + catch (e) { + return sendInternalServerError(res, e); + } + }); +} + +function enumerateTestFiles(runner: string) { + switch (runner) { + case "conformance": + case "compiler": + return listFiles(`tests/cases/${runner}`, /*serverDirname*/ undefined, /\.tsx?$/, { recursive: true }).map(parseCompilerTestConfigurations); + case "fourslash": + return listFiles(`tests/cases/fourslash`, /*serverDirname*/ undefined, /\.ts/i, { recursive: false }); + case "fourslash-shims": + return listFiles(`tests/cases/fourslash/shims`, /*serverDirname*/ undefined, /\.ts/i, { recursive: false }); + case "fourslash-shims-pp": + return listFiles(`tests/cases/fourslash/shims-pp`, /*serverDirname*/ undefined, /\.ts/i, { recursive: false }); + case "fourslash-server": + return listFiles(`tests/cases/fourslash/server`, /*serverDirname*/ undefined, /\.ts/i, { recursive: false }); + default: + throw new Error(`Runner '${runner}' not supported in browser tests.`); + } +} + +// Regex for parsing options in the format "@Alpha: Value of any sort" +const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines + +function extractCompilerSettings(content: string): Record { + const opts: Record = {}; + + let match: RegExpExecArray; + while ((match = optionRegex.exec(content)) !== null) { + opts[match[1]] = match[2].trim(); + } + + return opts; +} + +function splitVaryBySettingValue(text: string): string[] | undefined { + if (!text) return undefined; + const entries = text.split(/,/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0); + return entries && entries.length > 1 ? entries : undefined; +} + +function computeFileBasedTestConfigurationVariations(configurations: FileBasedTestConfiguration[], variationState: FileBasedTestConfiguration, varyByEntries: [string, string[]][], offset: number) { + if (offset >= varyByEntries.length) { + // make a copy of the current variation state + configurations.push({ ...variationState }); + return; + } + + const [varyBy, entries] = varyByEntries[offset]; + for (const entry of entries) { + // set or overwrite the variation + variationState[varyBy] = entry; + computeFileBasedTestConfigurationVariations(configurations, variationState, varyByEntries, offset + 1); + } +} + +function getFileBasedTestConfigurations(settings: Record, varyBy: string[]): FileBasedTestConfiguration[] | undefined { + let varyByEntries: [string, string[]][] | undefined; + for (const varyByKey of varyBy) { + if (Object.prototype.hasOwnProperty.call(settings, varyByKey)) { + const entries = splitVaryBySettingValue(settings[varyByKey]); + if (entries) { + if (!varyByEntries) varyByEntries = []; + varyByEntries.push([varyByKey, entries]); + } + } + } + + if (!varyByEntries) return undefined; + + const configurations: FileBasedTestConfiguration[] = []; + computeFileBasedTestConfigurationVariations(configurations, {}, varyByEntries, 0); + return configurations; +} + +function parseCompilerTestConfigurations(file: string): FileBasedTest { + const content = fs.readFileSync(path.join(rootDir, file), "utf8"); + const settings = extractCompilerSettings(content); + const configurations = getFileBasedTestConfigurations(settings, ["module", "target"]); + return { file, configurations }; +} + function handleApiListFiles(req: http.ServerRequest, res: http.ServerResponse) { readContent(req, (err, content) => { try { if (err) return sendInternalServerError(res, err); if (!content) return sendBadRequest(res); const serverPath = toServerPath(content); - const files: string[] = []; - visit(serverPath, content, files); + const files = listFiles(content, serverPath, /*spec*/ undefined, { recursive: true }); return sendJson(res, /*statusCode*/ 200, files); - function visit(dirname: string, relative: string, results: string[]) { - const { files, directories } = getAccessibleFileSystemEntries(dirname); - for (const file of files) { - results.push(path.join(relative, file)); - } - for (const directory of directories) { - visit(path.join(dirname, directory), path.join(relative, directory), results); - } - } } catch (e) { return sendInternalServerError(res, e); @@ -665,6 +771,26 @@ function handleApiListFiles(req: http.ServerRequest, res: http.ServerResponse) { }); } +function listFiles(clientDirname: string, serverDirname: string = path.resolve(rootDir, clientDirname), spec?: RegExp, options: { recursive?: boolean } = {}): string[] { + const files: string[] = []; + visit(serverDirname, clientDirname, files); + return files; + + function visit(dirname: string, relative: string, results: string[]) { + const { files, directories } = getAccessibleFileSystemEntries(dirname); + for (const file of files) { + if (!spec || file.match(spec)) { + results.push(path.join(relative, file)); + } + } + for (const directory of directories) { + if (options.recursive) { + visit(path.join(dirname, directory), path.join(relative, directory), results); + } + } + } +} + function handleApiDirectoryExists(req: http.ServerRequest, res: http.ServerResponse) { readContent(req, (err, content) => { try { @@ -707,6 +833,7 @@ function handlePostRequest(req: http.ServerRequest, res: http.ServerResponse) { switch (new URL(req.url, baseUrl).pathname) { case "/api/resolve": return handleApiResolve(req, res); case "/api/listFiles": return handleApiListFiles(req, res); + case "/api/enumerateTestFiles": return handleApiEnumerateTestFiles(req, res); case "/api/directoryExists": return handleApiDirectoryExists(req, res); case "/api/getAccessibleFileSystemEntries": return handleApiGetAccessibleFileSystemEntries(req, res); default: return sendMethodNotAllowed(res, ["HEAD", "GET", "PUT", "DELETE", "OPTIONS"]);