From 21e3f6812cc4ed29f816341c55de3fa09505a35c Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Wed, 14 Jun 2017 19:59:58 -0700 Subject: [PATCH] [WIP] harness rewrite --- .gitignore | 4 +- .vscode/tasks.json | 8 + Gulpfile.ts | 51 +- Jakefile.js | 55 +- package.json | 1 + scripts/mocha-file-reporter.js | 80 ++ scripts/mocha-parallel.js | 147 ++- src/compiler/utilities.ts | 2 +- src/harness2/api.ts | 215 ++++ src/harness2/baselines.ts | 29 + src/harness2/collections.ts | 142 +++ src/harness2/compiler.ts | 337 +++++++ src/harness2/config.ts | 35 + src/harness2/documents.ts | 39 + src/harness2/formatters/diagnostics.ts | 110 +++ src/harness2/formatters/index.ts | 6 + src/harness2/formatters/javaScript.ts | 57 ++ src/harness2/formatters/moduleResolution.ts | 7 + src/harness2/formatters/sourceMaps.ts | 123 +++ src/harness2/formatters/symbols.ts | 71 ++ src/harness2/formatters/types.ts | 53 + src/harness2/io-extended.ts | 391 ++++++++ src/harness2/io.ts | 221 +++++ src/harness2/options.ts | 76 ++ src/harness2/run.ts | 187 ++++ src/harness2/runner.ts | 74 ++ src/harness2/runners/compiler.ts | 299 ++++++ src/harness2/sourceMaps.ts | 164 ++++ src/harness2/testCaseParser.ts | 159 +++ src/harness2/textWriter.ts | 60 ++ src/harness2/tsconfig.json | 43 + src/harness2/utils.ts | 320 ++++++ src/harness2/vfs.ts | 917 ++++++++++++++++++ src/harness2/vpath.ts | 155 +++ src/services/utilities.ts | 54 ++ ...ierShouldNotShortCircuitBaseTypeBinding.js | 39 +- ...ouldNotShortCircuitBaseTypeBinding.symbols | 20 + ...ShouldNotShortCircuitBaseTypeBinding.types | 20 + tests/baselines/reference/genericArray0.js | 1 - .../baselines/reference/genericArray0.symbols | 15 +- tests/baselines/reference/genericArray0.types | 1 - .../moduleResolutionWithExtensions.symbols | 1 - .../moduleResolutionWithExtensions.types | 1 - .../module_augmentUninstantiatedModule2.js | 44 +- ...odule_augmentUninstantiatedModule2.symbols | 59 +- .../module_augmentUninstantiatedModule2.types | 56 +- .../reference/sourceMap-LineBreaks.js | 16 +- .../reference/sourceMap-LineBreaks.symbols | 41 +- .../reference/sourceMap-LineBreaks.types | 41 +- tests/cases/compiler/APISample_compile.ts | 2 + tests/cases/compiler/APISample_linter.ts | 2 + tests/cases/compiler/APISample_parseConfig.ts | 2 + tests/cases/compiler/APISample_transform.ts | 2 + tests/cases/compiler/APISample_watcher.ts | 2 + tests/cases/compiler/sourceMap-LineBreaks.ts | 1 + .../class/decoratedClassExportsCommonJS1.ts | 12 +- 56 files changed, 4945 insertions(+), 125 deletions(-) create mode 100644 scripts/mocha-file-reporter.js create mode 100644 src/harness2/api.ts create mode 100644 src/harness2/baselines.ts create mode 100644 src/harness2/collections.ts create mode 100644 src/harness2/compiler.ts create mode 100644 src/harness2/config.ts create mode 100644 src/harness2/documents.ts create mode 100644 src/harness2/formatters/diagnostics.ts create mode 100644 src/harness2/formatters/index.ts create mode 100644 src/harness2/formatters/javaScript.ts create mode 100644 src/harness2/formatters/moduleResolution.ts create mode 100644 src/harness2/formatters/sourceMaps.ts create mode 100644 src/harness2/formatters/symbols.ts create mode 100644 src/harness2/formatters/types.ts create mode 100644 src/harness2/io-extended.ts create mode 100644 src/harness2/io.ts create mode 100644 src/harness2/options.ts create mode 100644 src/harness2/run.ts create mode 100644 src/harness2/runner.ts create mode 100644 src/harness2/runners/compiler.ts create mode 100644 src/harness2/sourceMaps.ts create mode 100644 src/harness2/testCaseParser.ts create mode 100644 src/harness2/textWriter.ts create mode 100644 src/harness2/tsconfig.json create mode 100644 src/harness2/utils.ts create mode 100644 src/harness2/vfs.ts create mode 100644 src/harness2/vpath.ts diff --git a/.gitignore b/.gitignore index a6c8c2940d8..41c8d036b84 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,6 @@ internal/ !tests/cases/projects/NodeModulesSearch/**/* !tests/baselines/reference/project/nodeModules*/**/* .idea -yarn.lock \ No newline at end of file +yarn.lock +package-lock.json +.failed-tests \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f3c59a61717..52cf8dc8fad 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -18,6 +18,14 @@ "problemMatcher": [ "$tsc" ] + }, + { + "taskName": "harness", + "isBuildCommand": true, + "showOutput": "silent", + "problemMatcher": [ + "$tsc" + ] } ] } \ No newline at end of file diff --git a/Gulpfile.ts b/Gulpfile.ts index aef7cc4c6dd..0421ff164dc 100644 --- a/Gulpfile.ts +++ b/Gulpfile.ts @@ -38,7 +38,7 @@ const {runTestsInParallel} = mochaParallel; Error.stackTraceLimit = 1000; const cmdLineOptions = minimist(process.argv.slice(2), { - boolean: ["debug", "inspect", "light", "colors", "lint", "soft"], + boolean: ["debug", "inspect", "light", "colors", "lint", "soft", "failed", "bail", "keepFailed"], string: ["browser", "tests", "host", "reporter", "stackTraceLimit", "timeout"], alias: { b: "browser", @@ -49,6 +49,7 @@ const cmdLineOptions = minimist(process.argv.slice(2), { c: "colors", color: "colors", f: "files", file: "files", w: "workers", + "keep-failed": "keepFailed" }, default: { soft: false, @@ -57,9 +58,12 @@ const cmdLineOptions = minimist(process.argv.slice(2), { inspect: process.env.inspect || process.env["inspect-brk"] || process.env.i, host: process.env.TYPESCRIPT_HOST || process.env.host || "node", browser: process.env.browser || process.env.b || "IE", + bail: process.env.bail || false, timeout: process.env.timeout || 40000, tests: process.env.test || process.env.tests || process.env.t, light: process.env.light || false, + failed: process.env.failed || false, + keepFailed: process.env.keepFailed || process.env["keep-failed"] || false, reporter: process.env.reporter || process.env.r, lint: process.env.lint || true, files: process.env.f || process.env.file || process.env.files || "", @@ -93,6 +97,7 @@ const docDirectory = "doc/"; const builtDirectory = "built/"; const builtLocalDirectory = "built/local/"; +const builtHarnessDirectory = "built/harness/"; const LKGDirectory = "lib/"; const copyright = "CopyrightNotice.txt"; @@ -565,6 +570,23 @@ gulp.task(run, /*help*/ false, [servicesFile], () => { .pipe(gulp.dest("src/harness")); }); +const run2 = path.join(builtHarnessDirectory, "run.js"); +gulp.task(run2, /*help*/ false, [], () => { + const harnessProject = tsc.createProject("src/harness2/tsconfig.json", getCompilerSettings({ + types: ["node", "mocha", "chai"], + lib: ["es6"], + strict: true + }, /*useBuiltCompiler*/ false)); + return harnessProject.src() + .pipe(newer(builtHarnessDirectory)) + .pipe(sourcemaps.init()) + .pipe(harnessProject()) + .pipe(sourcemaps.write(".", { includeContent: false, destPath: builtHarnessDirectory })) + .pipe(gulp.dest(builtHarnessDirectory)); +}); + +gulp.task("harness", "Builds the test harness", [run2]); + const internalTests = "internal/"; const localBaseline = "tests/baselines/local/"; @@ -594,7 +616,7 @@ function restoreSavedNodeEnv() { process.env.NODE_ENV = savedNodeEnv; } -function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: (e?: any) => void) { +function runConsoleTests(defaultReporter: string, runInParallel: boolean, run: string, done: (e?: any) => void) { const lintFlag = cmdLineOptions["lint"]; cleanTestDirs((err) => { if (err) { console.error(err); failWithStatus(err, 1); } @@ -603,6 +625,9 @@ function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: const inspect = cmdLineOptions["inspect"]; const tests = cmdLineOptions["tests"]; const light = cmdLineOptions["light"]; + const bail = cmdLineOptions["bail"]; + const failed = cmdLineOptions["failed"]; + const keepFailed = cmdLineOptions["keepFailed"]; const stackTraceLimit = cmdLineOptions["stackTraceLimit"]; const testConfigFile = "test.config"; if (fs.existsSync(testConfigFile)) { @@ -637,16 +662,23 @@ function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: // default timeout is 2sec which really should be enough, but maybe we just need a small amount longer if (!runInParallel) { const args = []; - args.push("-R", reporter); + args.push("-R", "scripts/mocha-file-reporter"); + args.push("-O", '"reporter=' + reporter + (keepFailed ? ",keepFailed=true" : "") + '"'); if (tests) { args.push("-g", `"${tests}"`); } + else if (failed) { + args.push("--opts", ".failed-tests"); + } if (colors) { args.push("--colors"); } else { args.push("--no-colors"); } + if (bail) { + args.push("--bail"); + } if (inspect) { args.unshift("--inspect-brk"); } @@ -675,7 +707,7 @@ function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: } args.push(run); setNodeEnvToDevelopment(); - runTestsInParallel(taskConfigsFolder, run, { testTimeout: testTimeout, noColors: colors === " --no-colors " }, function(err) { + runTestsInParallel(taskConfigsFolder, run, { testTimeout: testTimeout, noColors: colors === " --no-colors ", keepFailed }, function(err) { // last worker clean everything and runs linter in case if there were no errors del(taskConfigsFolder).then(() => { if (!err) { @@ -720,13 +752,20 @@ function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: } gulp.task("runtests-parallel", "Runs all the tests in parallel using the built run.js file. Optional arguments are: --t[ests]=category1|category2|... --d[ebug]=true.", ["build-rules", "tests"], (done) => { - runConsoleTests("min", /*runInParallel*/ true, done); + runConsoleTests("min", /*runInParallel*/ true, run, done); }); gulp.task("runtests", "Runs the tests using the built run.js file. Optional arguments are: --t[ests]=regex --r[eporter]=[list|spec|json|] --d[ebug]=true --color[s]=false --lint=true.", ["build-rules", "tests"], (done) => { - runConsoleTests("mocha-fivemat-progress-reporter", /*runInParallel*/ false, done); + runConsoleTests("mocha-fivemat-progress-reporter", /*runInParallel*/ false, run, done); + }); + +gulp.task("runtests2", + "Runs the tests using the built run.js file. Optional arguments are: --t[ests]=regex --r[eporter]=[list|spec|json|] --d[ebug]=true --color[s]=false --lint=true.", + ["harness"], + (done) => { + runConsoleTests("mocha-fivemat-progress-reporter", /*runInParallel*/ false, run2, done); }); const nodeServerOutFile = "tests/webTestServer.js"; diff --git a/Jakefile.js b/Jakefile.js index 3de055b498e..72d32e187c0 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -20,6 +20,7 @@ var docDirectory = "doc/"; var builtDirectory = "built/"; var builtLocalDirectory = "built/local/"; +var builtHarnessDirectory = "built/harness/"; var LKGDirectory = "lib/"; var copyright = "CopyrightNotice.txt"; @@ -87,6 +88,7 @@ var typingsInstallerSources = filesFromConfig(path.join(serverDirectory, "typing var watchGuardSources = filesFromConfig(path.join(serverDirectory, "watchGuard/tsconfig.json")); var serverSources = filesFromConfig(path.join(serverDirectory, "tsconfig.json")) var languageServiceLibrarySources = filesFromConfig(path.join(serverDirectory, "tsconfig.library.json")); +var harness2Sources = filesFromConfig("./src/harness2/tsconfig.json"); var harnessCoreSources = [ "harness.ts", @@ -276,6 +278,7 @@ var builtLocalCompiler = path.join(builtLocalDirectory, compilerFilename); * @param {boolean} opts.stripInternal: true if compiler should remove declarations marked as @internal * @param {boolean} opts.inlineSourceMap: true if compiler should inline sourceMap * @param {Array} opts.types: array of types to include in compilation + * @param {boolean} opts.strict: true to compile using --strict * @param callback: a function to execute after the compilation process ends */ function compileFile(outFile, sources, prereqs, prefixes, useBuiltCompiler, opts, callback) { @@ -342,6 +345,9 @@ function compileFile(outFile, sources, prereqs, prefixes, useBuiltCompiler, opts else { options += " --lib es5" } + if (opts.strict) { + options += " --strict"; + } options += " --noUnusedLocals --noUnusedParameters"; var cmd = host + " " + compilerPath + " " + options + " "; @@ -715,6 +721,7 @@ task("LKG", ["clean", "release", "local"].concat(libraryTargets), function () { // Test directory directory(builtLocalDirectory); +directory(builtHarnessDirectory); // Task to build the tests infrastructure using the built compiler var run = path.join(builtLocalDirectory, "run.js"); @@ -726,6 +733,15 @@ compileFile( /*useBuiltCompiler:*/ true, /*opts*/ { inlineSourceMap: true, types: ["node", "mocha", "chai"], lib: "es6" }); +var run2 = path.join(builtHarnessDirectory, "run.js"); +compileFile( + /*outFile*/ run2, + /*source*/ harness2Sources, + /*prereqs*/[builtHarnessDirectory].concat(harness2Sources), + /*prefixes*/[], + /*useBuiltCompiler:*/ false, + /*opts*/ { inlineSourceMap: true, types: ["node", "mocha", "chai"], lib: "es6", noOutFile: true, outDir: builtHarnessDirectory, strict: true }); + var internalTests = "internal/"; var localBaseline = "tests/baselines/local/"; @@ -737,6 +753,9 @@ var refRwcBaseline = path.join(internalTests, "baselines/rwc/reference"); var localTest262Baseline = path.join(internalTests, "baselines/test262/local"); var refTest262Baseline = path.join(internalTests, "baselines/test262/reference"); +desc("Builds the test harness"); +task("harness", [run2]); + desc("Builds the test infrastructure using the built compiler"); task("tests", ["local", run].concat(libraryTargets)); @@ -801,7 +820,7 @@ function deleteTemporaryProjectOutput() { } } -function runConsoleTests(defaultReporter, runInParallel) { +function runConsoleTests(defaultReporter, runInParallel, run) { var dirty = process.env.dirty; if (!dirty) { cleanTestDirs(); @@ -811,6 +830,8 @@ function runConsoleTests(defaultReporter, runInParallel) { var inspect = process.env.inspect || process.env["inspect-brk"] || process.env.i; var testTimeout = process.env.timeout || defaultTestTimeout; var tests = process.env.test || process.env.tests || process.env.t; + var failed = process.env.failed || false; + var keepFailed = process.env.keepFailed || process.env["keep-failed"] || false; var light = process.env.light || false; var stackTraceLimit = process.env.stackTraceLimit; var testConfigFile = 'test.config'; @@ -847,12 +868,26 @@ function runConsoleTests(defaultReporter, runInParallel) { // timeout normally isn't necessary but Travis-CI has been timing out on compiler baselines occasionally // default timeout is 2sec which really should be enough, but maybe we just need a small amount longer if (!runInParallel) { + if (!tests && process.env.failed && process.env.failed.toLowerCase() === "true") { + const unique = Object.create(null); + tests = fs.readdirSync("tests/baselines/local") + .map(name => name + .replace(/(\.errors\.txt|\.jsx?(\.map)?|\.sourcemap\.txt|\.types|\.symbols)(\.delete)?$/, ".") + .replace(/[^\w\s\\-]/g, m => "\\" + m)) + .filter(name => unique[name] ? false : unique[name] = true) + .join("|"); + } + var startTime = mark(); var args = []; - args.push("-R", reporter); + args.push("-R", "scripts/mocha-file-reporter"); + args.push("-O", '"reporter=' + reporter + (keepFailed ? ",keepFailed=true" : "") + '"'); if (tests) { args.push("-g", `"${tests}"`); } + else if (failed) { + args.push("--opts", ".failed-tests"); + } if (colors) { args.push("--colors"); } @@ -894,12 +929,12 @@ function runConsoleTests(defaultReporter, runInParallel) { var savedNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = "development"; var startTime = mark(); - runTestsInParallel(taskConfigsFolder, run, { testTimeout: testTimeout, noColors: !colors }, function (err) { + runTestsInParallel(taskConfigsFolder, run, { testTimeout: testTimeout, noColors: !colors, keepFailed }, function (err) { process.env.NODE_ENV = savedNodeEnv; measure(startTime); // last worker clean everything and runs linter in case if there were no errors deleteTemporaryProjectOutput(); - jake.rmRf(taskConfigsFolder); + //jake.rmRf(taskConfigsFolder); if (err) { fail(err); } @@ -937,12 +972,20 @@ function runConsoleTests(defaultReporter, runInParallel) { desc("Runs all the tests in parallel using the built run.js file. Optional arguments are: t[ests]=category1|category2|... d[ebug]=true."); task("runtests-parallel", ["build-rules", "tests", builtLocalDirectory], function () { - runConsoleTests('min', /*runInParallel*/ true); + runConsoleTests('min', /*runInParallel*/ true, run); }, { async: true }); desc("Runs the tests using the built run.js file. Optional arguments are: t[ests]=regex r[eporter]=[list|spec|json|] d[ebug]=true color[s]=false lint=true bail=false dirty=false."); task("runtests", ["build-rules", "tests", builtLocalDirectory], function() { - runConsoleTests('mocha-fivemat-progress-reporter', /*runInParallel*/ false); + runConsoleTests('mocha-fivemat-progress-reporter', /*runInParallel*/ false, run); +}, { async: true }); + +task("runtests2", [run2], function () { + runConsoleTests('dot', /*runInParallel*/ false, run2); +}, { async: true }); + +task("runtests-parallel2", [run2], function () { + runConsoleTests('min', /*runInParallel*/ true, run2); }, { async: true }); desc("Generates code coverage data via instanbul"); diff --git a/package.json b/package.json index 5b11935992f..9dd1d154e54 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/node": "latest", "@types/q": "latest", "@types/run-sequence": "latest", + "@types/source-map-support": "^0.4.0", "@types/through2": "latest", "browserify": "latest", "chai": "latest", diff --git a/scripts/mocha-file-reporter.js b/scripts/mocha-file-reporter.js new file mode 100644 index 00000000000..739e2b34848 --- /dev/null +++ b/scripts/mocha-file-reporter.js @@ -0,0 +1,80 @@ +var Mocha = require('mocha'); +var path = require('path'); +var fs = require('fs'); + +exports = module.exports = FileReporter; + +function FileReporter(runner, options) { + if (!runner) return; + options = options || {}; + + var reporterOptions = this.reporterOptions = options.reporterOptions || {}; + reporterOptions.file = reporterOptions.file || ".failed-tests"; + reporterOptions.keepFailed = reporterOptions.keepFailed || false; + if (reporterOptions.reporter) { + var _reporter; + if (typeof reporterOptions.reporter === "function") { + _reporter = reporterOptions.reporter; + } + else if (Mocha.reporters[reporterOptions.reporter]) { + _reporter = Mocha.reporters[reporterOptions.reporter]; + } + else { + try { + _reporter = require(reporterOptions.reporter); + } + catch (err) { + _reporter = require(path.resolve(process.cwd(), reporterOptions.reporter)); + } + } + + var newOptions = {}; + for (var p in options) newOptions[p] = options[p]; + newOptions.reporterOptions = reporterOptions.reporterOptions || {}; + this.reporter = new _reporter(runner, newOptions); + } + + var failures = this.failures = []; + var tests = this.tests = []; + runner.on('test end', function (test) { + tests.push(test); + }) + runner.on('fail', function (test, err) { + failures.push(test); + }); +} + +FileReporter.prototype.done = function (numFailures, fn) { + FileReporter.writeFailures(this.reporterOptions.file, this.failures, this.reporterOptions.keepFailed || this.tests.length === 0, done); + + function done(err) { + var reporter = this.reporter; + if (reporter && reporter.done) { + reporter.done(numFailures, fn); + } + else { + if (fn) fn(numFailures); + } + + if (err) console.error(err); + } +} + +FileReporter.writeFailures = function (fileName, failures, keepFailed, fn) { + if (keepFailed) { + fn(); + return; + } + if (failures.length) { + var failed = failures.map(function (test) { return escapeRegExp(test.fullTitle()); }).join("|"); + fs.writeFile(fileName, "--grep " + failed, "utf8", fn); + } + else { + fs.unlink(fileName, function () { fn(); }); + } +}; + +var reservedCharacterRegExp = /[^\w]/g; +function escapeRegExp(pattern) { + return pattern.replace(reservedCharacterRegExp, function (match) { return "\\" + match; }); +} \ No newline at end of file diff --git a/scripts/mocha-parallel.js b/scripts/mocha-parallel.js index ce40c832b21..77be4ca7ff2 100644 --- a/scripts/mocha-parallel.js +++ b/scripts/mocha-parallel.js @@ -1,14 +1,15 @@ var tty = require("tty") - , readline = require("readline") - , fs = require("fs") - , path = require("path") - , child_process = require("child_process") - , os = require("os") - , mocha = require("mocha") - , Base = mocha.reporters.Base - , color = Base.color - , cursor = Base.cursor - , ms = require("mocha/lib/ms"); + , readline = require("readline") + , fs = require("fs") + , path = require("path") + , child_process = require("child_process") + , os = require("os") + , mocha = require("mocha") + , Base = mocha.reporters.Base + , color = Base.color + , cursor = Base.cursor + , ms = require("mocha/lib/ms") + , FileReporter = require("./mocha-file-reporter"); var isatty = tty.isatty(1) && tty.isatty(2); var tapRangePattern = /^(\d+)\.\.(\d+)(?:$|\r\n?|\n)/; @@ -19,7 +20,7 @@ exports.runTestsInParallel = runTestsInParallel; exports.ProgressBars = ProgressBars; function runTestsInParallel(taskConfigsFolder, run, options, cb) { - if (options === undefined) options = { }; + if (options === undefined) options = {}; return discoverTests(run, options, function (error) { if (error) { @@ -33,7 +34,12 @@ function runTestsInParallel(taskConfigsFolder, run, options, cb) { function discoverTests(run, options, cb) { console.log("Discovering tests..."); - var cmd = "mocha -R " + require.resolve("./mocha-none-reporter.js") + " " + run; + var args = []; + args.push("--no-timeouts"); + args.push("--delay"); // prevents mocha from running tests. + args.push(run); + args.push("--discover"); // signal discovery (must come after files) + var cmd = "mocha " + args.join(" "); var p = spawnProcess(cmd); p.on("exit", function (status) { if (status) { @@ -46,25 +52,19 @@ function discoverTests(run, options, cb) { } function runTests(taskConfigsFolder, run, options, cb) { - var configFiles = fs.readdirSync(taskConfigsFolder); - var numPartitions = configFiles.length; - if (numPartitions <= 0) { - cb(); - return; - } - - console.log("Running tests on " + numPartitions + " threads..."); - - var partitions = Array(numPartitions); - var progressBars = new ProgressBars(); - progressBars.enable(); - - var counter = numPartitions; - configFiles.forEach(runTestsInPartition); - - function runTestsInPartition(file, index) { - var partition = { - file: path.join(taskConfigsFolder, file), + var partitions = fs.readdirSync(taskConfigsFolder).map(function (file, index) { + var file = path.join(taskConfigsFolder, file); + var args = []; + args.push("-t", options.testTimeout || 40000); + args.push("-R", "tap"); + args.push("--no-colors"); + args.push(run); + args.push("--config='" + file + "'"); + var cmd = "mocha " + args.join(" "); + return { + file: file, + cmd: cmd, + index: index, tests: 0, passed: 0, failed: 0, @@ -72,23 +72,48 @@ function runTests(taskConfigsFolder, run, options, cb) { current: undefined, start: undefined, end: undefined, + catastrophicError: "", failures: [] }; - partitions[index] = partition; + }); + if (partitions.length <= 0) { + cb(); + return; + } + + console.log("Running tests on " + partitions.length + " threads..."); + + var progressBars = new ProgressBars(); + progressBars.enable(); + + var counter = partitions.length; + partitions.forEach(runTestsInPartition); + + function runTestsInPartition(partition) { // Set up the progress bar. updateProgress(0); // Start the background process. - var cmd = "mocha -t " + (options.testTimeout || 20000) + " -R tap --no-colors " + run + " --config='" + partition.file + "'"; - var p = spawnProcess(cmd); + var p = spawnProcess(partition.cmd); var rl = readline.createInterface({ input: p.stdout, terminal: false }); + + var rlError = readline.createInterface({ + input: p.stderr, + terminal: false + }); + rl.on("line", onmessage); + rlError.on("line", onErrorMessage); p.on("exit", onexit) + function onErrorMessage(line) { + partition.catastrophicError += line + os.EOL; + } + function onmessage(line) { if (partition.start === undefined) { partition.start = Date.now(); @@ -153,15 +178,17 @@ function runTests(taskConfigsFolder, run, options, cb) { } } - function onexit() { + function onexit(code) { if (partition.end === undefined) { partition.end = Date.now(); } partition.duration = partition.end - partition.start; - var summaryColor = partition.failed ? "fail" : "green"; - var summarySymbol = partition.failed ? Base.symbols.err : Base.symbols.ok; - var summaryTests = (partition.passed === partition.tests ? partition.passed : partition.passed + "/" + partition.tests) + " passing"; + var isPartitionFail = partition.failed || code !== 0; + var summaryColor = isPartitionFail ? "fail" : "green"; + var summarySymbol = isPartitionFail ? Base.symbols.err : Base.symbols.ok; + + var summaryTests = (isPartitionFail ? partition.passed + "/" + partition.tests : partition.passed) + " passing"; var summaryDuration = "(" + ms(partition.duration) + ")"; var savedUseColors = Base.useColors; Base.useColors = !options.noColors; @@ -181,7 +208,7 @@ function runTests(taskConfigsFolder, run, options, cb) { } progressBars.update( - index, + partition.index, percentComplete, progressColor, title @@ -198,12 +225,34 @@ function runTests(taskConfigsFolder, run, options, cb) { failures = reporter.failures; var duration = 0; - for (var i = 0; i < numPartitions; i++) { + var catastrophicError = ""; + for (var i = 0; i < partitions.length; i++) { var partition = partitions[i]; stats.passes += partition.passed; stats.failures += partition.failed; stats.tests += partition.tests; duration += partition.duration; + if (partition.catastrophicError !== "") { + // Partition is written out to a temporary file as a JSON object. + // Below is an example of how the partition JSON object looks like + // { + // "light":false, + // "tasks":[ + // { + // "runner":"compiler", + // "files":["tests/cases/compiler/es6ImportNamedImportParsingError.ts"] + // } + // ], + // "runUnitTests":false + // } + var jsonText = fs.readFileSync(partition.file); + var configObj = JSON.parse(jsonText); + if (configObj.tasks && configObj.tasks[0]) { + catastrophicError += "Error from one or more of these files: " + configObj.tasks[0].files + os.EOL; + catastrophicError += partition.catastrophicError; + catastrophicError += os.EOL; + } + } for (var j = 0; j < partition.failures.length; j++) { var failure = partition.failures[j]; failures.push(makeMochaTest(failure)); @@ -223,18 +272,18 @@ function runTests(taskConfigsFolder, run, options, cb) { reporter.epilogue(); } - if (stats.failures) { - return cb(new Error("Test failures reported: " + stats.failures)); - } - else { + FileReporter.writeFailures(".failed-tests", failures, options.keepFailed, function (err) { + if (err) return cb(err); + if (catastrophicError !== "") return cb(new Error(catastrophicError)); + if (stats.failures) return cb(new Error("Test failures reported: " + stats.failures)); return cb(); - } + }); } } function makeMochaTest(test) { return { - fullTitle: function() { + fullTitle: function () { return test.name; }, err: { @@ -247,9 +296,9 @@ function runTests(taskConfigsFolder, run, options, cb) { var nodeModulesPathPrefix = path.resolve("./node_modules/.bin/") + path.delimiter; if (process.env.path !== undefined) { - process.env.path = nodeModulesPathPrefix + process.env.path; + process.env.path = nodeModulesPathPrefix + process.env.path; } else if (process.env.PATH !== undefined) { - process.env.PATH = nodeModulesPathPrefix + process.env.PATH; + process.env.PATH = nodeModulesPathPrefix + process.env.PATH; } function spawnProcess(cmd, options) { @@ -294,7 +343,7 @@ ProgressBars.prototype = { update: function (index, percentComplete, color, title) { percentComplete = minMax(percentComplete, 0, 1); - var progressBar = this._progressBars[index] || (this._progressBars[index] = { }); + var progressBar = this._progressBars[index] || (this._progressBars[index] = {}); var width = this._options.width; var n = Math.floor(width * percentComplete); var i = width - n; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 3833fc30875..3b5c058311c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2581,7 +2581,7 @@ namespace ts { // JavaScript files are always LanguageVariant.JSX, as JSX syntax is allowed in .js files also. // So for JavaScript files, '.jsx' is only emitted if the input was '.jsx', and JsxEmit.Preserve. // For TypeScript, the only time to emit with a '.jsx' extension, is on JSX input, and JsxEmit.Preserve - function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): Extension { + export function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): Extension { if (options.jsx === JsxEmit.Preserve) { if (isSourceFileJavaScript(sourceFile)) { if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) { diff --git a/src/harness2/api.ts b/src/harness2/api.ts new file mode 100644 index 00000000000..c22fab86487 --- /dev/null +++ b/src/harness2/api.ts @@ -0,0 +1,215 @@ +// This file contains the minimal definitions for TypeScript needed to host the +// compiler for the purpose of running tests. + +// Re-export the built compiler using a CommonJS-style export. This is necessary as we +// want 'api' to masquerade as a minimal surface for the built compiler without +// taking a compile-time dependency to avoid long compilation times. +module.exports = require("../../built/local/typescript.js"); + +export interface MapLike { + [index: string]: T | undefined; +} + +export interface DiagnosticMessageChain { + messageText: string; + category: number; + code: number; + next?: DiagnosticMessageChain; +} + +export interface Diagnostic { + file: SourceFile | undefined; + start: number | undefined; + length: number | undefined; + messageText: string | DiagnosticMessageChain; + category: number; + code: number; + source?: string; +} + +export interface PluginImport { + name: string; +} + +export interface CommandLineOptionBase { + name: string; + type: "string" | "number" | "boolean" | "object" | "list" | Map; +} + +export interface CommandLineOptionOfPrimitiveType extends CommandLineOptionBase { + type: "string" | "number" | "boolean"; +} + +export interface CommandLineOptionOfCustomType extends CommandLineOptionBase { + type: Map; // an object literal mapping named values to actual values +} + +export interface CommandLineOptionOfListType extends CommandLineOptionBase { + type: "list"; + element: CommandLineOptionOfCustomType | CommandLineOptionOfPrimitiveType; +} + +export type CommandLineOption = CommandLineOptionOfCustomType | CommandLineOptionOfPrimitiveType | CommandLineOptionOfListType; + +export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[]; + +export interface CompilerOptions { + declaration?: boolean; + declarationDir?: string; + inlineSourceMap?: boolean; + mapRoot?: string; + newLine?: NewLineKind; + noEmitOnError?: boolean; + noEmit?: boolean; + noErrorTruncation?: boolean; + out?: string; + outDir?: string; + outFile?: string; + skipDefaultLibCheck?: boolean; + sourceMap?: boolean; + target?: ScriptTarget; + traceResoluton?: boolean; + [option: string]: CompilerOptionsValue | undefined; +} + +export interface ParsedCommandLine { + options: CompilerOptions; + fileNames: string[]; + errors: Diagnostic[]; +} + +export interface SourceMapSpan { + emittedLine: number; + emittedColumn: number; + sourceLine: number; + sourceColumn: number; + sourceIndex: number; + nameIndex?: number; +} + +export interface SourceMapData { + jsSourceMappingURL: string; + inputSourceFileNames: string[]; + sourceMapFilePath: string; + sourceMapFile: string; + sourceMapSourceRoot: string; + sourceMapSources: string[]; + sourceMapSourcesContent?: string[]; + sourceMapNames?: string[]; + sourceMapMappings: string; + sourceMapDecodedMappings: SourceMapSpan[]; +} + +export interface EmitResult { + emitSkipped: boolean; + diagnostics: Diagnostic[]; + emittedFiles: string[]; + sourceMaps: SourceMapData[]; +} + +export interface ModuleResolutionHost { + fileExists(fileName: string): boolean; + readFile(fileName: string): string | undefined; + trace?(s: string): void; + directoryExists?(directoryName: string): boolean; + realpath?(path: string): string; + getCurrentDirectory?(): string; + getDirectories?(path: string): string[]; +} + +export interface ScriptReferenceHost { + getCompilerOptions(): CompilerOptions; + getSourceFile(fileName: string): SourceFile | undefined; + getSourceFileByPath(path: string): SourceFile | undefined; + getCurrentDirectory(): string; +} + +export interface ParseConfigHost { + useCaseSensitiveFileNames: boolean; + readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[]): string[]; + fileExists(path: string): boolean; + readFile(path: string): string | undefined; +} + +export interface CompilerHost extends ModuleResolutionHost { + getSourceFile(fileName: string, languageVersion: number, onError?: (message: string) => void): SourceFile | undefined; + getDefaultLibFileName(options: CompilerOptions): string; + getDefaultLibLocation(): string; + writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: SourceFile[]): void; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + getCanonicalFileName(fileName: string): string; + useCaseSensitiveFileNames(): boolean; + getNewLine(): string; +} + +export interface Program extends ScriptReferenceHost { + getRootFileNames(): string[]; + getSourceFiles(): SourceFile[]; + emit(): EmitResult; + getOptionsDiagnostics(): Diagnostic[]; + getGlobalDiagnostics(): Diagnostic[]; + getSyntacticDiagnostics(): Diagnostic[]; + getSemanticDiagnostics(): Diagnostic[]; + getDeclarationDiagnostics(): Diagnostic[]; + // getTypeChecker(): TypeChecker; + + getCommonSourceDirectory(): string; +} + +export interface SourceFile { + fileName: string; + text: string; +} + +export interface TypesAndSymbols { + line: number; + text: string; + type: string | undefined; + symbol: string | undefined; + declarations: { fileName: string, line: number, character: number }[] | undefined; +} + +export interface FormatDiagnosticsHost { + getCurrentDirectory(): string; + getCanonicalFileName(fileName: string): string; + getNewLine(): string; +} + +export declare enum DiagnosticCategory { +} + +export declare enum ScriptTarget { + ES3, + ES5, + ES2015, + ES2016, + ES2017, + ESNext +} + +export declare enum NewLineKind { + CarriageReturnLineFeed +} + +export declare namespace Debug { + const isDebugging: boolean; + function enableDebugInfo(): void; +} + +export declare const optionDeclarations: CommandLineOption[]; +export declare function compareDiagnostics(d1: Diagnostic, d2: Diagnostic): number; +export declare function fixupCompilerOptions(options: CompilerOptions, diagnostics: Diagnostic[]): CompilerOptions; +export declare function flattenDiagnosticMessageText(messageText: string | DiagnosticMessageChain, newLine: string): string; +export declare function formatDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHost): string; +export declare function getDefaultLibFileName(options: CompilerOptions): string; +export declare function parseConfigFileTextToJson(fileName: string, jsonText: string, stripComments?: boolean): { config?: any; error?: Diagnostic }; +export declare function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string): ParsedCommandLine; +export declare function parseListTypeOption(opt: CommandLineOptionOfListType, value: string | undefined, errors: Diagnostic[]): (string | number)[] | undefined; +export declare function parseCustomTypeOption(opt: CommandLineOptionOfCustomType, value: string, errors: Diagnostic[]): string | number | undefined; +export declare function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program; +export declare function getPreEmitDiagnostics(program: Program): Diagnostic[]; +export declare function createSourceFile(fileName: string, sourceText: string, languageVersion: number, setParentNodes?: boolean): SourceFile; +export declare function matchFiles(path: string, extensions: string[], excludes: string[], includes: string[], useCaseSensitiveFileNames: boolean, currentDirectory: string, getFileSystemEntries: (path: string) => { files: string[], directories: string[] }): string[]; +export declare function getTypesAndSymbols(program: Program, fileName: string, checked: boolean, exclude: "types" | "symbols" | undefined): TypesAndSymbols[]; +export declare function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): string; \ No newline at end of file diff --git a/src/harness2/baselines.ts b/src/harness2/baselines.ts new file mode 100644 index 00000000000..cb9cf7f4e08 --- /dev/null +++ b/src/harness2/baselines.ts @@ -0,0 +1,29 @@ +import * as vpath from "./vpath"; +import * as io from "./io"; +import { toUtf8 } from "./utils"; +import { assert } from "chai"; + +export interface BaselineOptions { + base?: string; + relative?: string; + local?: string; + reference?: string; +} + +export function baseline(file: string, actual: string | undefined, opts: BaselineOptions = {}) { + const local = vpath.combine(opts.base || "tests/baselines", opts.relative || "", opts.local || "local", file); + const reference = vpath.combine(opts.base || "tests/baselines", opts.relative || "", opts.reference || "reference", file); + const expected = io.readFile(reference); + io.createDirectory(vpath.dirname(local)); + io.deleteFile(local); + if (actual !== undefined) actual = toUtf8(actual); + if (expected !== actual) { + if (actual === undefined) { + io.writeFile(local + ".delete", ""); + } + else { + io.writeFile(local, actual); + } + assert.fail(actual, expected, `The baseline file ${file} has changed.`); + } +} \ No newline at end of file diff --git a/src/harness2/collections.ts b/src/harness2/collections.ts new file mode 100644 index 00000000000..c8e45be4b28 --- /dev/null +++ b/src/harness2/collections.ts @@ -0,0 +1,142 @@ +import { compareValues, binarySearch, insertAt, removeAt } from "./utils"; + +export class KeyedCollection { + private _comparer: (a: K, b: K) => number; + private _keys: K[] = []; + private _values: V[] = []; + private _order: number[] = []; + private _version = 0; + + constructor(comparer: (a: K, b: K) => number = compareValues) { + this._comparer = comparer; + } + + public get size() { + return this._keys.length; + } + + public has(key: K) { + return binarySearch(this._keys, key, this._comparer) >= 0; + } + + public get(key: K) { + const index = binarySearch(this._keys, key, this._comparer); + return index >= 0 ? this._values[index] : undefined; + } + + public set(key: K, value: V) { + const index = binarySearch(this._keys, key, this._comparer); + if (index >= 0) { + this._values[index] = value; + } + else { + insertAt(this._keys, ~index, key); + insertAt(this._values, ~index, value); + insertAt(this._order, ~index, this._version); + this._version++; + } + return this; + } + + public delete(key: K) { + const index = binarySearch(this._keys, key, this._comparer); + if (index >= 0) { + removeAt(this._keys, index); + removeAt(this._values, index); + removeAt(this._order, index); + this._version++; + return true; + } + return false; + } + + public clear() { + this._keys.length = 0; + this._values.length = 0; + this._order.length = 0; + this._version = 0; + } + + public forEach(callback: (value: V, key: K, collection: this) => void) { + let order = this.getInsertionOrder(); + let version = this._version; + for (let i = 0; i < order.length; i++) { + callback(this._values[order[i]], this._keys[order[i]], this); + if (version !== this._version) { + order = this.getInsertionOrder(); + version = this._version; + } + } + } + + private getInsertionOrder() { + return this._order + .map((_, i) => i) + .sort((x, y) => compareValues(this._order[x], this._order[y])); + } +} + +const undefinedSentinel = {}; + +export class Metadata { + private _parent: Metadata | undefined; + private _map: { [key: string]: any }; + private _version = 0; + private _size: number | undefined; + private _parentVersion: number | undefined; + + constructor(parent?: Metadata) { + this._parent = parent; + this._map = Object.create(parent ? parent._map : null); // tslint:disable-line:no-null-keyword + } + + public get size(): number { + if (this._size === undefined || (this._parent && this._parent._version !== this._parentVersion)) { + let size = 0; + for (const _ in this._map) size++; + this._size = size; + if (this._parent) { + this._parentVersion = this._parent._version; + } + } + return this._size; + } + + public has(key: string): boolean { + return this._map[key] !== undefined; + } + + public get(key: string): any { + const value = this._map[key]; + return value === undefinedSentinel ? undefined : value; + } + + public set(key: string, value: any): this { + this._map[key] = value === undefined ? undefinedSentinel : value; + this._version++; + this._size = undefined; + return this; + } + + public delete(key: string): boolean { + if (this._map[key] !== undefined) { + delete this._map[key]; + this._version++; + this._size = undefined; + return true; + } + return false; + } + + public clear(): void { + this._map = Object.create(this._parent ? this._parent._map : null); // tslint:disable-line:no-null-keyword + this._version++; + this._size = undefined; + } + + public forEach(callback: (value: any, key: string, map: this) => void) { + for (const key in this._map) { + callback(this._map[key], key, this); + } + } +} \ No newline at end of file diff --git a/src/harness2/compiler.ts b/src/harness2/compiler.ts new file mode 100644 index 00000000000..4843e4b4039 --- /dev/null +++ b/src/harness2/compiler.ts @@ -0,0 +1,337 @@ +import * as ts from "./api"; +import * as vpath from "./vpath"; +import { VirtualFileSystem } from "./vfs"; +import { TextDocument, isJavaScriptDocument, isDeclarationDocument, isSourceMapDocument } from "./documents"; +import { isDeclarationFile, compareStrings, isDefaultLibraryFile, isJsonFile, removeComments, stripBOM } from "./utils"; +import { SourceMap } from "./sourceMaps"; +import { KeyedCollection } from "./collections"; + +export class CompilerHost { + private _setParentNodes: boolean; + private _sourceFiles = new Map(); + private _newLine: string; + + // public readonly ts: ts.TypeScript; + public readonly vfs: VirtualFileSystem; + public readonly defaultLibLocation: string; + public readonly outputs: TextDocument[] = []; + public readonly traces: string[] = []; + + constructor(vfs: VirtualFileSystem, defaultLibLocation: string, newLine: "crlf" | "lf", setParentNodes = false) { + this.vfs = vfs; + this.defaultLibLocation = defaultLibLocation; + this._newLine = newLine === "crlf" ? "\r\n" : "\n"; + this._setParentNodes = setParentNodes; + } + + public getCurrentDirectory(): string { + return this.vfs.currentDirectory; + } + + public useCaseSensitiveFileNames(): boolean { + return this.vfs.useCaseSensitiveFileNames; + } + + public getNewLine(): string { + return this._newLine; + } + + public getCanonicalFileName(fileName: string): string { + return this.vfs.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); + } + + public fileExists(fileName: string): boolean { + return this.vfs.fileExists(fileName); + } + + public directoryExists(directoryName: string): boolean { + return this.vfs.directoryExists(directoryName); + } + + public getDirectories(path: string): string[] { + const entry = this.vfs.getDirectory(path); + return entry ? entry.getDirectories().map(dir => dir.name) : []; + } + + public readFile(path: string): string | undefined { + const entry = this.vfs.getFile(path); + let content = entry && entry.getContent(); + if (content) { + content = stripBOM(content); + return isJsonFile(path) ? removeComments(content) : content; + } + return undefined; + } + + public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) { + // NOTE(rbuckton): Old harness emits "\u00EF\u00BB\u00BF" for BOM, but compiler emits "\uFEFF". Compiler is wrong, as "\uFEFF" is a UTF16 BOM. + if (writeByteOrderMark) content = "\u00EF\u00BB\u00BF" + content; + const entry = this.vfs.addFile(fileName, content, { overwrite: true }); + if (entry) { + const document = new TextDocument(entry.path, content); + entry.metadata.set("document", document); + const index = this.outputs.findIndex(doc => this.vfs.sameName(document.file, doc.file)); + if (index < 0) { + this.outputs.push(document); + } + else { + this.outputs[index] = document; + } + } + } + + public trace(s: string): void { + this.traces.push(s); + } + + public realpath(path: string): string { + const entry = this.vfs.getEntry(path, { followSymlinks: true }); + return entry && entry.path || path; + } + + public getDefaultLibLocation(): string { + return vpath.resolve(this.vfs.currentDirectory, this.defaultLibLocation); + } + + public getDefaultLibFileName(options: ts.CompilerOptions): string { + // FIXME(rbuckton): The old harness overrides the behavior for getting the default lib file + // name instead of using the correct one. Instead, we should be using `ts.getDefaultLibFileName`: + + // return vpath.resolve(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options)); + return vpath.resolve(this.getDefaultLibLocation(), this.getDefaultLibFileNameWorker(options)); + } + + private getDefaultLibFileNameWorker(options: ts.CompilerOptions) { + switch (options.target) { + case ts.ScriptTarget.ESNext: + case ts.ScriptTarget.ES2017: + return "lib.es2017.d.ts"; + case ts.ScriptTarget.ES2016: + return "lib.es2016.d.ts"; + case ts.ScriptTarget.ES2015: + return "lib.es2015.d.ts"; + default: + return "lib.d.ts"; + } + } + + public getSourceFile(fileName: string, languageVersion: number): ts.SourceFile | undefined { + fileName = this.getCanonicalFileName(vpath.resolve(this.vfs.currentDirectory, fileName)); + + const existing = this._sourceFiles.get(fileName); + if (existing) return existing; + + const file = this.vfs.getFile(fileName); + // FIXME(rbuckton): The old harness reads _lib.es5.d.ts_ in place of _lib.d.ts_. We really + // shouldn't be doing that. + const patchedFileName = this.vfs.sameName(fileName, "/.ts/lib.d.ts") ? "/.ts/lib.es5.d.ts" : fileName; + const patchedFile = patchedFileName !== fileName ? this.vfs.getFile(patchedFileName) : file; + if (file && patchedFile) { + let content = patchedFile.getContent(); + if (content !== undefined) { + content = stripBOM(content); + // We cache and reuse source files for files we know shouldn't change. + const shouldCache = isDefaultLibraryFile(fileName) || + vpath.beneath("/.ts", file.path, !this.vfs.useCaseSensitiveFileNames) || + vpath.beneath("/.lib", file.path, !this.vfs.useCaseSensitiveFileNames); + + const cacheKey = shouldCache && `SourceFile[languageVersion=${languageVersion},setParentNodes=${this._setParentNodes}]`; + if (cacheKey) { + const sourceFileFromMetadata = file.metadata.get(cacheKey) as ts.SourceFile | undefined; + if (sourceFileFromMetadata) { + this._sourceFiles.set(fileName, sourceFileFromMetadata); + return sourceFileFromMetadata; + } + } + + const parsed = ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes); + this._sourceFiles.set(fileName, parsed); + + if (cacheKey) { + // store the cached source file on the unshadowed file. + let rootFile = file; + while (rootFile.shadowRoot) rootFile = rootFile.shadowRoot; + rootFile.metadata.set(cacheKey, parsed); + } + + return parsed; + } + } + } +} + +export interface CompilationOutput { + readonly input: TextDocument; + readonly js: TextDocument | undefined; + readonly dts: TextDocument | undefined; + readonly map: TextDocument | undefined; +} + +export class CompilationResult { + public readonly host: CompilerHost; + public readonly program: ts.Program; + public readonly result: ts.EmitResult; + public readonly diagnostics: ts.Diagnostic[]; + public readonly js: KeyedCollection; + public readonly dts: KeyedCollection; + public readonly maps: KeyedCollection; + + private _inputsAndOutputs: KeyedCollection; + + constructor(host: CompilerHost, program: ts.Program, result: ts.EmitResult, diagnostics: ts.Diagnostic[]) { + this.host = host; + this.program = program; + this.result = result; + this.diagnostics = diagnostics; + + // collect outputs + const pathComparer = this.vfs.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive; + this.js = new KeyedCollection(pathComparer); + this.dts = new KeyedCollection(pathComparer); + this.maps = new KeyedCollection(pathComparer); + for (const document of this.host.outputs) { + if (isJavaScriptDocument(document)) { + this.js.set(document.file, document); + } + else if (isDeclarationDocument(document)) { + this.dts.set(document.file, document); + } + else if (isSourceMapDocument(document)) { + this.maps.set(document.file, document); + } + } + + // correlate inputs and outputs + this._inputsAndOutputs = new KeyedCollection(pathComparer); + for (const sourceFile of program.getSourceFiles()) { + if (sourceFile && !isDeclarationFile(sourceFile.fileName)) { + const file = host.vfs.getFile(sourceFile.fileName); + + let input = file && file.metadata.get("document"); + if (!input) { + input = new TextDocument(file ? file.path : sourceFile.fileName, file && file.getContent() || sourceFile.text); + if (file) file.metadata.set("document", input); + } + + const outputs = { + input, + js: this.js.get(this.getOutputPath(sourceFile.fileName, ts.getOutputExtension(sourceFile, this.options))), + dts: this.dts.get(this.getOutputPath(sourceFile.fileName, ".d.ts", this.options.declarationDir)), + map: this.maps.get(this.getOutputPath(sourceFile.fileName, ts.getOutputExtension(sourceFile, this.options) + ".map")) + }; + + this._inputsAndOutputs.set(sourceFile.fileName, outputs); + if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs); + if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs); + if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs); + } + } + } + + public get vfs() { + return this.host.vfs; + } + + public get options() { + return this.program.getCompilerOptions(); + } + + public get outputs() { + return this.host.outputs; + } + + public get traces(): string[] { + return this.host.traces; + } + + public get emitSkipped(): boolean { + return this.result.emitSkipped; + } + + public get singleFile() { + return !!this.options.outFile || !!this.options.out; + } + + public get commonSourceDirectory() { + const common = this.program.getCommonSourceDirectory(); + return common && vpath.combine(this.vfs.currentDirectory, common); + } + + public getInputsAndOutputs(path: string) { + return this._inputsAndOutputs.get(vpath.resolve(this.vfs.currentDirectory, path)); + } + + public getInput(path: string) { + const outputs = this.getInputsAndOutputs(path); + return outputs && outputs.input; + } + + public getOutput(path: string, kind: "js" | "dts" | "map") { + const outputs = this.getInputsAndOutputs(path); + return outputs && outputs[kind]; + } + + public getSourceMap(path: string) { + if (this.options.noEmit || isDeclarationFile(path)) return undefined; + if (this.options.inlineSourceMap) { + const document = this.getOutput(path, "js"); + return document && SourceMap.fromSource(document.text); + } + if (this.options.sourceMap) { + const document = this.getOutput(path, "map"); + return document && new SourceMap(document.file, document.text); + } + } + + public getOutputPath(path: string, ext: string, outDir: string | undefined = this.options.outDir) { + if (outDir) { + path = vpath.resolve(this.vfs.currentDirectory, path); + const common = this.program.getCommonSourceDirectory(); + if (!common) return vpath.chext(path, ext); + path = vpath.relative(common, path, !this.vfs.useCaseSensitiveFileNames); + path = vpath.combine(vpath.resolve(this.vfs.currentDirectory, outDir), path); + return vpath.chext(path, ext); + } + const outFile = vpath.resolve(this.vfs.currentDirectory, this.options.outFile || this.options.out || path); + return vpath.chext(outFile, ext); + } +} + +export class ParseConfigHost { + public readonly vfs: VirtualFileSystem; + + constructor(vfs: VirtualFileSystem) { + this.vfs = vfs; + } + + public get useCaseSensitiveFileNames() { + return this.vfs.useCaseSensitiveFileNames; + } + + public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[]): string[] { + return ts.matchFiles(path, extensions, excludes, includes, this.vfs.useCaseSensitiveFileNames, this.vfs.currentDirectory, path => this.vfs.getAccessibleFileSystemEntries(path)); + } + + public fileExists(path: string) { + return this.vfs.fileExists(path); + } + + public readFile(path: string) { + const entry = this.vfs.getFile(path); + return entry && entry.getContent(); + } +} + +export function compileFiles(vfs: VirtualFileSystem, defaultLibLocation: string, rootFiles: string[], options: ts.CompilerOptions) { + // establish defaults (aligns with old harness) + if (options.target === undefined) options.target = ts.ScriptTarget.ES3; + if (options.newLine === undefined) options.newLine = ts.NewLineKind.CarriageReturnLineFeed; + if (options.skipDefaultLibCheck === undefined) options.skipDefaultLibCheck = true; + if (options.noErrorTruncation === undefined) options.noErrorTruncation = true; + + const host = new CompilerHost(vfs, defaultLibLocation, options.newLine === ts.NewLineKind.CarriageReturnLineFeed ? "crlf" : "lf"); + const program = ts.createProgram(rootFiles, options, host); + const emitResult = program.emit(); + const errors = ts.getPreEmitDiagnostics(program); + return new CompilationResult(host, program, emitResult, errors); +} \ No newline at end of file diff --git a/src/harness2/config.ts b/src/harness2/config.ts new file mode 100644 index 00000000000..ff841f07dec --- /dev/null +++ b/src/harness2/config.ts @@ -0,0 +1,35 @@ +import * as io from "./io"; +import { ParsedArguments } from "./options"; +import { TestRunTask } from "./runner"; + +export interface TestConfig { + light?: boolean; + taskConfigsFolder?: string; + workerCount?: number; + stackTraceLimit?: number | "full"; + tasks?: TestRunTask[]; + test?: string[]; + runUnitTests?: boolean; +} + +/** + * Get the current test configuration. + */ +export function getTestConfig(args: ParsedArguments): TestConfig { + const content = getTestConfigContent(); + const config: TestConfig = content ? JSON.parse(content) : { }; + if (config.light === undefined) config.light = false; + if (config.workerCount === undefined) config.workerCount = 0; + if (config.runUnitTests === undefined) config.runUnitTests = !args.discover; + return config; + + /** + * Read the test configuration from either the command line, a + * custom _mytest.config_ file, or the default _test.config_ file. + */ + function getTestConfigContent(): string | undefined { + return args.config && io.readFile(args.config) + || io.readFile("mytest.config") + || io.readFile("test.config"); + } +} \ No newline at end of file diff --git a/src/harness2/documents.ts b/src/harness2/documents.ts new file mode 100644 index 00000000000..5ed9f091de2 --- /dev/null +++ b/src/harness2/documents.ts @@ -0,0 +1,39 @@ +import { isTypeScriptFile, isJavaScriptFile, isDeclarationFile, isSourceMapFile, isJsonFile, computeLineStarts } from "./utils"; + +export class TextDocument { + public readonly meta: Map; + public readonly file: string; + public readonly text: string; + + private _lineStarts: number[] | undefined; + + constructor(file: string, content: string, meta?: Map) { + this.file = file; + this.text = content; + this.meta = meta || new Map(); + } + + public get lineStarts(): number[] { + return this._lineStarts || (this._lineStarts = computeLineStarts(this.text)); + } +} + +export function isTypeScriptDocument(document: TextDocument) { + return isTypeScriptFile(document.file); +} + +export function isJavaScriptDocument(document: TextDocument) { + return isJavaScriptFile(document.file); +} + +export function isDeclarationDocument(document: TextDocument) { + return isDeclarationFile(document.file); +} + +export function isSourceMapDocument(document: TextDocument) { + return isSourceMapFile(document.file); +} + +export function isJsonDocument(document: TextDocument) { + return isJsonFile(document.file); +} \ No newline at end of file diff --git a/src/harness2/formatters/diagnostics.ts b/src/harness2/formatters/diagnostics.ts new file mode 100644 index 00000000000..05509fb2f6b --- /dev/null +++ b/src/harness2/formatters/diagnostics.ts @@ -0,0 +1,110 @@ +import * as ts from "../api"; +import * as vpath from "../vpath"; +import { TextDocument } from "../documents"; +import { TextWriter } from "../textWriter"; +import { isDefaultLibraryFile, repeatString, isBuiltFile, splitLines, compareValues, removeTestPathPrefixes, getLinesAndLineStarts } from "../utils"; +import { assert } from "chai"; +import { CompilationResult } from "../compiler"; + +function compareDiagnostics(a: ts.Diagnostic, b: ts.Diagnostic) { + if (a.file && b.file) { + return compareValues(!isDefaultLibraryFile(a.file.fileName), !isDefaultLibraryFile(b.file.fileName)) + || ts.compareDiagnostics(a, b); + } + return ts.compareDiagnostics(a, b); +} + +export function formatDiagnostics(documents: TextDocument[], result: CompilationResult) { + const diagnostics = result.diagnostics; + diagnostics.sort(compareDiagnostics); + + let numNonLibraryDiagnostics = 0; + let numLibraryDiagnostics = 0; + let numTest262HarnessDiagnostics = 0; + + const writer = new TextWriter(removeTestPathPrefixes(ts.formatDiagnostics(diagnostics, { + getCanonicalFileName: path => path, + getCurrentDirectory: () => "", + getNewLine: () => "\r\n" + }))); + + writer.writeln(); + + // write global diagnostics first + for (const diagnostic of diagnostics) { + if (!diagnostic.file) { + writeDiagnostic(diagnostic); + } + else if (isDefaultLibraryFile(diagnostic.file.fileName) || isBuiltFile(diagnostic.file.fileName)) { + numLibraryDiagnostics++; + } + else if (diagnostic.file.fileName.includes("test262-harness")) { + numTest262HarnessDiagnostics++; + } + } + + // write file diagnostics + for (const document of documents) { + const path = vpath.resolve(result.vfs.currentDirectory, document.file); + const fileDiagnostics = diagnostics.filter(diagnostic => { + const file = diagnostic.file; + return file !== undefined && result.vfs.sameName(file.fileName, path); + }); + + writer.writeln().write(`==== ${document.file} (${fileDiagnostics.length} errors) ====`); + let numMarkedDiagnostics = 0; + + // For each line, emit the line followed by any error squiggles matching this line + const { lines, lineStarts } = getLinesAndLineStarts(document.text); + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + const thisLineStart = lineStarts[lineIndex]; + const nextLineStart = lineIndex === lines.length - 1 ? document.text.length : lineStarts[lineIndex + 1]; + // Emit this line from the original file + writer.writeln().write(` ${line}`); + for (const diagnostic of fileDiagnostics) { + if (diagnostic.start === undefined || diagnostic.length === undefined) continue; + const end = diagnostic.start + diagnostic.length; + // Does any error start or continue on to this line? Emit squiggles + if (end >= thisLineStart && (diagnostic.start < nextLineStart || lineIndex === lines.length - 1)) { + // How many characters from the start of this line the error starts at (could be positive or negative) + const relativeOffset = diagnostic.start - thisLineStart; + // How many characters of the error are on this line (might be longer than this line in reality) + const length = diagnostic.length - Math.max(0, thisLineStart - diagnostic.start); + // Calculate the start of the squiggle + const squiggleStart = Math.max(0, relativeOffset); + const squiggleLength = Math.min(length, line.length - squiggleStart); + const prefix = line.slice(0, squiggleStart).replace(/\S/g, " "); + const squiggle = repeatString("~", squiggleLength); + writer.writeln().write(` ${prefix}${squiggle}`); + + // If the error ended here, or we're at the end of the file, emit its message + if ((lineIndex === lines.length - 1) || nextLineStart > end) { + writeDiagnostic(diagnostic); + numMarkedDiagnostics++; + } + } + } + } + + assert.lengthOf(fileDiagnostics, numMarkedDiagnostics, `Incorrect number of marked errors in ${document.file}`); + } + + assert.lengthOf(diagnostics, numNonLibraryDiagnostics + numLibraryDiagnostics + numTest262HarnessDiagnostics, "total number of errors"); + return writer.toString(); + + function writeDiagnostic(diagnostic: ts.Diagnostic) { + writer.writeln().write(formatDiagnostic(/*ts,*/ diagnostic)); + if (!diagnostic.file || !isDefaultLibraryFile(diagnostic.file.fileName)) { + numNonLibraryDiagnostics++; + } + } +} + +function formatDiagnostic(diagnostic: ts.Diagnostic) { + const category = ts.DiagnosticCategory[diagnostic.category]; + return splitLines(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\r\n"), /*removeEmptyElements*/ true) + .map(line => `!!! ${category ? category.toLowerCase() : ""} TS${diagnostic.code}: ${line}`) + .join("\r\n") + .replace(/\/\.test\//g, ""); +} \ No newline at end of file diff --git a/src/harness2/formatters/index.ts b/src/harness2/formatters/index.ts new file mode 100644 index 00000000000..6c02900cae6 --- /dev/null +++ b/src/harness2/formatters/index.ts @@ -0,0 +1,6 @@ +export { formatJavaScript } from "./javaScript"; +export { formatDiagnostics } from "./diagnostics"; +export { formatSourceMapData, formatSourceMaps } from "./sourceMaps"; +export { formatModuleResolution } from "./moduleResolution"; +export { formatTypes } from "./types"; +export { formatSymbols } from "./symbols"; \ No newline at end of file diff --git a/src/harness2/formatters/javaScript.ts b/src/harness2/formatters/javaScript.ts new file mode 100644 index 00000000000..d4c862e3406 --- /dev/null +++ b/src/harness2/formatters/javaScript.ts @@ -0,0 +1,57 @@ +import * as vpath from "../vpath"; +import { TextDocument } from "../documents"; +import { TextWriter } from "../textWriter"; +import { CompilationResult } from "../compiler"; +import { formatDiagnostics } from "./diagnostics"; + +export function formatJavaScript(header: string, fullEmitPaths: boolean, documents: TextDocument[], result: CompilationResult, declarationDocuments: TextDocument[] | undefined, declarationResult: CompilationResult | undefined) { + const writer = new TextWriter(); + + // add header if needed + if (documents.length > 1) writer.writeln(`//// [${header}] ////`); + + // add each input document + for (const document of documents) { + if (writer.size > 0) writer.writeln(); + writer.writeln(`//// [${vpath.basename(document.file)}]`); + writer.write(document.text); + } + + // add space between ts and js/dts emit + if (result.js.size > 0 || result.dts.size > 0 || (declarationResult && declarationResult.diagnostics.length > 0)) { + writer.writeln(); + writer.writeln(); + } + + // add each script output + result.js.forEach(document => { + const file = fullEmitPaths ? document.file : vpath.basename(document.file); + writer.writeln(`//// [${file}]`); + writer.write(document.text); + }); + + // Add space between js and dts emit + if (result.js.size > 0 && result.dts.size > 0) { + writer.writeln(); + writer.writeln(); + } + + // add each declaration output + result.dts.forEach(document => { + const file = fullEmitPaths ? document.file : vpath.basename(document.file); + writer.writeln(`//// [${file}]`); + writer.write(document.text); + }); + + // add declaration diagnostics + if (declarationDocuments && declarationResult && declarationResult.diagnostics.length > 0) { + writer.writeln(); + writer.writeln(); + writer.writeln(`//// [DtsFileErrors]`); + writer.writeln(); + writer.writeln(); + writer.write(formatDiagnostics(declarationDocuments, declarationResult)); + } + + return writer.toString(); +} \ No newline at end of file diff --git a/src/harness2/formatters/moduleResolution.ts b/src/harness2/formatters/moduleResolution.ts new file mode 100644 index 00000000000..567d5042e8b --- /dev/null +++ b/src/harness2/formatters/moduleResolution.ts @@ -0,0 +1,7 @@ +import { CompilationResult } from "../compiler"; +import { removeTestPathPrefixes } from "../utils"; + +export function formatModuleResolution(result: CompilationResult) { + const lines = result.traces.map(removeTestPathPrefixes); + return JSON.stringify(lines, /*replacer*/ undefined, " "); +} diff --git a/src/harness2/formatters/sourceMaps.ts b/src/harness2/formatters/sourceMaps.ts new file mode 100644 index 00000000000..960818a3f6d --- /dev/null +++ b/src/harness2/formatters/sourceMaps.ts @@ -0,0 +1,123 @@ +import * as vpath from "../vpath"; +import { assert } from "chai"; +import { TextWriter } from "../textWriter"; +import { CompilationResult } from "../compiler"; +import { SourceMap, Mapping } from "../sourceMaps"; +import { TextDocument } from "../documents"; +import { computeLineStarts, padRight, repeatString } from "../utils"; + +export function formatSourceMapData(result: CompilationResult) { + const writer = new TextWriter(); + result.js.forEach(emittedDocument => { + const sourceMap = result.getSourceMap(emittedDocument.file); + if (sourceMap) { + writer.writeln("==================================================================="); + writer.writeln("JsFile: " + vpath.basename(emittedDocument.file)); + const mapUrl = !result.options.inlineSourceMap ? SourceMap.getUrl(emittedDocument.text) : undefined; + writer.writeln("mapUrl: " + mapUrl); + writer.writeln("sourceRoot: " + sourceMap.sourceMap.sourceRoot); + writer.writeln("sources: " + sourceMap.sourceMap.sources); + if (sourceMap.sourceMap.sourcesContent) { + writer.writeln("sourcesContent: " + JSON.stringify(sourceMap.sourceMap.sourcesContent)); + } + writer.writeln("==================================================================="); + + const sources = sourceMap.sources.map(source => { + const file = vpath.combine(result.vfs.currentDirectory, result.commonSourceDirectory, source.file); + const document = result.getInput(file)!; + assert.isDefined(document); + const lineStarts = computeLineStarts(document.text); + return { document, lineStarts }; + }); + + const emittedLineStarts = emittedDocument.lineStarts; + for (let i = 0; i < emittedLineStarts.length; i++) { + const lineStart = emittedLineStarts[i]; + const lineEnd = i < emittedLineStarts.length - 1 ? emittedLineStarts[i + 1] : emittedDocument.text.length; + const line = emittedDocument.text.slice(lineStart, lineEnd); + writer.write(`>>>${line}`); + + const mappings = sourceMap.getMappingsForEmittedLine(i); + if (mappings) { + let column = 0; + for (let i = 0; i < mappings.length; i++) { + const mapping = mappings[i]; + writer.write(`${padRight("" + (i + 1), 2)}>`); + writer.write(repeatString(" ", column)); + writer.write(repeatString("^", mapping.emittedColumn - column)); + writer.writeln(); + column = mapping.emittedColumn; + } + + column = 0; + let sourceColumn = 0; + for (let i = 0; i < mappings.length; i++) { + const mapping = mappings[i]; + const source = sources[mapping.sourceIndex]; + const sourceLineStart = source.lineStarts[mapping.sourceLine]; + const sourceLineEnd = mapping.sourceLine < source.lineStarts.length - 1 ? source.lineStarts[mapping.sourceLine + 1] : source.document.text.length; + const sourceLine = source.document.text.slice(sourceLineStart, sourceLineEnd); + writer.write(`${padRight("" + (i + 1), 2)}>`); + writer.write(repeatString(" ", column)); + writer.write(sourceLine.slice(sourceColumn, mapping.sourceColumn)); + writer.writeln(); + sourceColumn = mapping.sourceColumn; + column = mapping.emittedColumn; + } + + column = 0; + for (let i = 0; i < mappings.length; i++) { + const mapping = mappings[i]; + writer.write(`${padRight("" + (i + 1), 2)}>`); + writer.write(`Emitted(${mapping.emittedLine + 1}, ${mapping.emittedColumn + 1}) Source(${mapping.sourceLine + 1}, ${mapping.sourceColumn + 1}) + SourceIndex(${mapping.sourceIndex})`); + if (mapping.name) { + writer.write(` name (${mapping.name})`); + } + else if (mapping.nameIndex !== undefined) { + writer.write(` nameIndex (${mapping.nameIndex})`); + } + writer.writeln(); + column = mapping.emittedColumn; + } + } + // if (mappings && mappings.length) { + // for (const mapping of mappings) { + // if (mapping.sourceIndex !== currentSourceIndex) { + // const file = vpath.combine(result.vfs.currentDirectory, result.commonSourceDirectory, mapping.source.file); + // const source = result.getInput(file)!; + // assert.isDefined(source); + // writer.writeln("-------------------------------------------------------------------"); + // writer.writeln("emittedFile:" + document.file); + // writer.writeln("sourceFile:" + mapping.source.file); + // writer.writeln("-------------------------------------------------------------------"); + // currentSource = source; + // currentSourceIndex = mapping.sourceIndex; + // hasEmittedMappings = true; + // } + // } + // } + // if (hasEmittedMappings) { + // while (lastEmittedLine < i) { + // const lineStart = emittedLineStarts[lastEmittedLine]; + // const lineEnd = lastEmittedLine < emittedLineStarts.length - 1 ? emittedLineStarts[lastEmittedLine + 1] : document.text.length; + // const line = document.text.slice(lineStart, lineEnd); + // writer.write(`>>>${line}`); + // lastEmittedLine++; + // } + // } + } + } + }); + + return writer.toString(); +} + +export function formatSourceMaps(fullEmitPaths: boolean, result: CompilationResult) { + const writer = new TextWriter(); + result.maps.forEach(document => { + const file = fullEmitPaths ? document.file : vpath.basename(document.file); + writer.writeln(`//// [${file}]`); + writer.write(document.text); + }); + return writer.toString(); +} \ No newline at end of file diff --git a/src/harness2/formatters/symbols.ts b/src/harness2/formatters/symbols.ts new file mode 100644 index 00000000000..88a85611c73 --- /dev/null +++ b/src/harness2/formatters/symbols.ts @@ -0,0 +1,71 @@ +import * as ts from "../api"; +import * as vpath from "../vpath"; +import { TextDocument, isTypeScriptDocument, isJavaScriptDocument } from "../documents"; +import { TextWriter } from "../textWriter"; +import { splitLines, removeTestPathPrefixes } from "../utils"; + +const leadingOrTrailingBraceRegExp = /^\s*[{}]\s*$/; +export function formatSymbols(documents: TextDocument[], typesAndSymbols: Map) { + const writer = new TextWriter(); + for (const document of documents) { + if (!isTypeScriptDocument(document) && !isJavaScriptDocument(document)) continue; + writer.writeln(`=== ${document.file} ===`); + const results = typesAndSymbols.get(document.file); + let lineMap: Map | undefined; + if (results) { + for (const result of results) { + if (!result.symbol) continue; + if (!lineMap) lineMap = new Map(); + let lineInfo = lineMap.get(result.line); + if (!lineInfo) lineMap.set(result.line, lineInfo = []); + lineInfo.push(result.text.replace(/\r\n?|\n/g, "") + " : " + formatSymbol(result.symbol, result.declarations)); + } + } + // TODO(rbuckton): We should consider switching to the following commented code below + // to address the FIXME immediately below. + // if (!lineMap) { + // writer.writeln("No symbol information for this code."); + // continue; + // } + const lines = splitLines(document.text); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + writer.writeln(line); + if (!lineMap) { + // FIXME(rbuckton): The old harness writes this over and over. We may want to prefer the + // commented version above instead. + writer.write("No type information for this code."); + continue; + } + const lineInfo = lineMap.get(i); + if (lineInfo) { + for (const symbol of lineInfo) { + writer.writeln(`>${removeTestPathPrefixes(symbol)}`); + } + const nextLine = i < lines.length - 1 ? lines[i + 1] : undefined; + if (nextLine === undefined || (!leadingOrTrailingBraceRegExp.test(nextLine) && nextLine.trim())) { + writer.writeln(); + } + } + } + } + return writer.toString(); +} + +function formatSymbol(symbol: string, declarations: { fileName: string, line: number, character: number }[] | undefined) { + const writer = new TextWriter(); + writer.write("Symbol("); + writer.write(symbol); + if (declarations) { + for (const decl of declarations) { + const basename = vpath.basename(decl.fileName); + // FIXME(rbuckton): The following incorrectly catches `tslib.d.ts`. We should consider + // switching to the commented line below. + const isLibFile = /lib(.*)\.d\.ts/i.test(basename); + // const isLibFile = isDefaultLibraryFile(basename); + writer.write(`, Decl(${basename}, ${isLibFile ? "--" : decl.line}, ${isLibFile ? "--" : decl.character})`); + } + } + writer.write(")"); + return writer.toString(); +} \ No newline at end of file diff --git a/src/harness2/formatters/types.ts b/src/harness2/formatters/types.ts new file mode 100644 index 00000000000..8bf5ea2c673 --- /dev/null +++ b/src/harness2/formatters/types.ts @@ -0,0 +1,53 @@ +import * as ts from "../api"; +import { TextDocument, isTypeScriptDocument, isJavaScriptDocument } from "../documents"; +import { TextWriter } from "../textWriter"; +import { splitLines, removeTestPathPrefixes } from "../utils"; +import { assert } from "chai"; + +const leadingOrTrailingBraceRegExp = /^\s*[{}]\s*$/; +export function formatTypes(documents: TextDocument[], typesAndSymbols: Map) { + const writer = new TextWriter(); + for (const document of documents) { + if (!isTypeScriptDocument(document) && !isJavaScriptDocument(document)) continue; + writer.writeln(`=== ${document.file} ===`); + const results = typesAndSymbols.get(document.file); + let lineMap: Map | undefined; + if (results) { + for (const result of results) { + assert.isDefined(result.type, "type doesn't exist"); + if (!lineMap) lineMap = new Map(); + let lineInfo = lineMap.get(result.line); + if (!lineInfo) lineMap.set(result.line, lineInfo = []); + lineInfo.push(result.text.replace(/\r\n?|\n/g, "") + " : " + result.type); + } + } + // TODO(rbuckton): We should consider switching to the following commented code below + // to address the FIXME immediately below. + // if (!lineMap) { + // writer.writeln("No type information for this code."); + // continue; + // } + const lines = splitLines(document.text); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + writer.writeln(line); + if (!lineMap) { + // FIXME(rbuckton): The old harness writes this over and over. We may want to prefer the + // commented version above instead. + writer.write("No type information for this code."); + continue; + } + const lineInfo = lineMap.get(i); + if (lineInfo) { + for (const type of lineInfo) { + writer.writeln(`>${removeTestPathPrefixes(type)}`); + } + const nextLine = i < lines.length - 1 ? lines[i + 1] : undefined; + if (nextLine === undefined || (!leadingOrTrailingBraceRegExp.test(nextLine) && nextLine.trim())) { + writer.writeln(); + } + } + } + } + return writer.toString(); +} \ No newline at end of file diff --git a/src/harness2/io-extended.ts b/src/harness2/io-extended.ts new file mode 100644 index 00000000000..07451581811 --- /dev/null +++ b/src/harness2/io-extended.ts @@ -0,0 +1,391 @@ +import * as Utils from "./utils"; +import * as VirtualPath from "./vpath"; +import { VirtualFileSystem } from "./vfs"; + +interface IO { + newLine(): string; + getCurrentDirectory(): string; + useCaseSensitiveFileNames(): boolean; + resolvePath(path: string): string | undefined; + readFile(path: string): string | undefined; + writeFile(path: string, contents: string): void; + directoryName(path: string): string | undefined; + getDirectories(path: string): string[]; + createDirectory(path: string): void; + fileExists(fileName: string): boolean; + directoryExists(path: string): boolean; + deleteFile(fileName: string): void; + listFiles(path: string, filter: RegExp, options?: { recursive?: boolean }): string[]; + log(text: string): void; + getMemoryUsage?(): number; + args(): string[]; + getExecutingFilePath(): string; + exit(exitCode?: number): void; + readDirectory(path: string, extension?: string[], exclude?: string[], include?: string[]): string[]; + tryEnableSourceMapsForHost?(): void; + getEnvironmentVariable?(name: string): string; +} + +let matchFiles: ((path: string, extensions: string[] | undefined, excludes: string[] | undefined, includes: string[] | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string, getFileSystemEntries: (path: string) => { files: string[], directories: string[] }) => string[]) | undefined; + +export function setFileMatcher(value: (path: string, extensions: string[], excludes: string[], includes: string[], useCaseSensitiveFileNames: boolean, currentDirectory: string, getFileSystemEntries: (path: string) => { files: string[], directories: string[] }) => string[]) { + matchFiles = value; +} + +function createNodeIO(): IO { + const fs = require("fs"); + const path = require("path"); + const os = require("os"); + const platform = os.platform(); + const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + const args = process.argv.slice(2); + const executingFilePath = path.join(__dirname, "run.js"); + return { + newLine() { + return "\r\n"; + }, + useCaseSensitiveFileNames() { + return useCaseSensitiveFileNames; + }, + getCurrentDirectory() { + return process.cwd(); + }, + getExecutingFilePath() { + return executingFilePath; + }, + args() { + return args; + }, + log(text: string) { + console.log(text); + }, + exit(exitCode?: number) { + process.exit(exitCode); + }, + resolvePath(name: string) { + return path.resolve(name); + }, + readFile, + writeFile, + directoryName, + getDirectories, + createDirectory(name: string) { + tryExec(() => fs.mkdirSync(name)); + }, + fileExists, + directoryExists, + deleteFile(name: string) { + tryExec(() => fs.unlinkSync(name)); + }, + listFiles(dirname: string, spec?: RegExp, options?: { recursive?: boolean }): string[] { + return filesInFolder(dirname, options && options.recursive || false); + function filesInFolder(folder: string, recursive: boolean): string[] { + let paths: string[] = []; + const files = fs.readdirSync(folder); + for (let i = 0; i < files.length; i++) { + const pathToFile = path.join(folder, files[i]); + const stat = fs.statSync(pathToFile); + if (recursive && stat.isDirectory()) { + paths = paths.concat(filesInFolder(pathToFile, /*recursive*/ true)); + } + else if (stat.isFile() && (!spec || files[i].match(spec))) { + paths.push(pathToFile); + } + } + + return paths; + } + }, + getMemoryUsage() { + if (global.gc) { + global.gc(); + } + return process.memoryUsage().heapUsed; + }, + readDirectory, + tryEnableSourceMapsForHost() { + tryExec(() => require("source-map-support").install()); + }, + getEnvironmentVariable(name: string) { + return process.env[name] || ""; + } + }; + + function isFileSystemCaseSensitive() { + if (platform === "win32" || platform === "win64") { + return false; + } + return !fileExists(__filename.toUpperCase()) + || !fileExists(__filename.toLowerCase()); + } + + function tryExec(func: () => T): T | undefined { + try { + return func(); + } + catch (e) { + return undefined; + } + } + + function fileExists(name: string): boolean { + return tryExec(() => fs.statSync(name).isFile()) || false; + } + + function directoryExists(name: string): boolean { + return tryExec(() => fs.statSync(name).isDirectory()) || false; + } + + function readFile(name: string) { + return tryExec(() => { + const buffer = fs.readFileSync(name); + let len = buffer.length; + if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) { + // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js, + // flip all byte pairs and treat as little endian. + len &= ~1; // Round down to a multiple of 2 + for (let i = 0; i < len; i += 2) { + const temp = buffer[i]; + buffer[i] = buffer[i + 1]; + buffer[i + 1] = temp; + } + return buffer.toString("utf16le", 2); + } + if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { + // Little endian UTF-16 byte order mark detected + return buffer.toString("utf16le", 2); + } + if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + // UTF-8 byte order mark detected + return buffer.toString("utf8", 3); + } + // Default is UTF-8 with no byte order mark + return buffer.toString("utf8"); + }); + } + + function writeFile(name: string, contents: string) { + fs.writeFileSync(name, contents, "utf8"); + } + + function directoryName(name: string) { + const dir = path.dirname(name); + // Node will just continue to repeat the root path, rather than return null + return dir === name ? undefined : dir; + } + + function getDirectories(name: string): string[] { + return fs.readdirSync(name).filter((dir: string) => directoryExists(path.join(name, dir))); + } + + function readDirectory(path: string, extensions?: string[], excludes?: string[], includes?: string[]): string[] { + if (!matchFiles) { + throw new Error("File matcher not defined."); + } + return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), getAccessibleFileSystemEntries); + } + + function getAccessibleFileSystemEntries(dirname: string): { files: string[], directories: string[] } { + try { + const entries = fs.readdirSync(dirname || ".").sort(); + const files: string[] = []; + const directories: string[] = []; + for (const entry of entries) { + // This is necessary because on some file system node fails to exclude + // "." and "..". See https://github.com/nodejs/node/issues/4002 + if (entry === "." || entry === "..") { + continue; + } + const name = path.join(dirname, entry); + + let stat: any; + try { + stat = fs.statSync(name); + } + catch (e) { + continue; + } + + if (stat.isFile()) { + files.push(entry); + } + else if (stat.isDirectory()) { + directories.push(entry); + } + } + return { files, directories }; + } + catch (e) { + return { files: [], directories: [] }; + } + } +} + +declare var XMLHttpRequest: { + new (): XMLHttpRequest; +}; + +interface XMLHttpRequest { + readonly readyState: number; + readonly responseText: string; + readonly status: number; + open(method: string, url: string, async?: boolean, user?: string, password?: string): void; + send(data?: string): void; + setRequestHeader(header: string, value: string): void; +} + +function createNetworkIO(): IO { + const serverRoot = "http://localhost:8888/"; + const args: string[] = []; + const vfs = new VirtualFileSystem("", false); + + interface Response { + status: number; + responseText?: string; + } + + return { + newLine() { + return "\r\n"; + }, + useCaseSensitiveFileNames() { + return vfs.useCaseSensitiveFileNames; + }, + getCurrentDirectory() { + return vfs.currentDirectory; + }, + getExecutingFilePath() { + return ""; + }, + args() { + return args; + }, + log(s: string) { + console.log(s); + }, + exit(_?: number) { + }, + resolvePath(name: string) { + const response = send("POST", `${serverRoot}${name}?action=resolve`); + return response.status === 200 ? response.responseText : undefined; + }, + readFile(name: string) { + const response = send("GET", `${serverRoot}${name}`); + return response.status === 200 ? response.responseText : undefined; + }, + writeFile(name: string, contents: string) { + send("PUT", `${serverRoot}${name}`, contents); + }, + directoryName(name: string) { + return VirtualPath.dirname(name); + }, + getDirectories(_: string): string[] { + return []; + }, + createDirectory(_: string) { }, + fileExists(name: string) { + return send("HEAD", `${serverRoot}${name}`).status === 200; + }, + directoryExists(_: string) { + return false; + }, + deleteFile(name: string) { + send("POST", `${serverRoot}${name}?action=DELETE`); + }, + listFiles, + readDirectory + }; + + function send(method: "GET" | "HEAD" | "PUT" | "POST" | "DELETE", url: string, content?: string): Response { + try { + const xhr = new XMLHttpRequest(); + xhr.open(method, url, /*async*/ false); + xhr.setRequestHeader("Access-Control-Allow-Origin", "*"); + xhr.send(content); + while (xhr.readyState !== 4); + return xhr; + } + catch (e) { + console.log(`XHR Error: ${e}`); + return { status: 500 }; + } + } + + // function directoryNameImpl(path: string) { + // let dirPath = path; + // // root of the server + // if (dirPath.match(/localhost:\d+$/) || dirPath.match(/localhost:\d+\/$/)) { + // dirPath = undefined; + // // path + fileName + // } + // else if (dirPath.indexOf(".") === -1) { + // dirPath = dirPath.substring(0, dirPath.lastIndexOf("/")); + // // path + // } + // else { + // // strip any trailing slash + // if (dirPath.match(/.*\/$/)) { + // dirPath = dirPath.substring(0, dirPath.length - 2); + // } + // dirPath = dirPath.substring(0, dirPath.lastIndexOf("/")); + // } + + // return dirPath; + // } + + function listFiles(_path: string, _spec?: RegExp): string[] { + throw new Error("Not implemented"); + // let vdir = vfs.getDirectory(path); + // if (vdir === undefined) { + // const response = send("GET", `${serverRoot}${path}`); + // if (response.status === 200 && response.responseText) { + // const results = response.responseText.split(","); + + // return spec ? results.filter(file => spec.test(file)) : results; + // } + // } + // if (vdir) { + // const results = vdir.getFiles(true).map(file => file.fullName); + // return spec ? results.filter(file => spec.test(file)) : results; + // } + // return []; + } + + function readDirectory(_path: string, _extensions?: string[], _excludes?: string[], _includes?: string[]): string[] { + throw new Error("Not implemented"); + // return matchFiles(path, extensions, excludes, includes, vfs.useCaseSensitiveFileNames, process.cwd(), getAccessibleFileSystemEntries); + } +} + +function createIO() { + const environment = Utils.getExecutionEnvironment(); + switch (environment) { + case Utils.ExecutionEnvironment.Node: return createNodeIO(); + case Utils.ExecutionEnvironment.Browser: return createNetworkIO(); + default: throw new Error(`Unknown value '${environment}' for ExecutionEnvironment.`); + } +} + +export const { + newLine, + useCaseSensitiveFileNames, + getCurrentDirectory, + getExecutingFilePath, + args, + log, + exit, + resolvePath, + readFile, + writeFile, + directoryName, + getDirectories, + createDirectory, + fileExists, + directoryExists, + deleteFile, + listFiles, + getMemoryUsage, + readDirectory, + tryEnableSourceMapsForHost, + getEnvironmentVariable +} = createIO(); diff --git a/src/harness2/io.ts b/src/harness2/io.ts new file mode 100644 index 00000000000..5a18b1d9326 --- /dev/null +++ b/src/harness2/io.ts @@ -0,0 +1,221 @@ +import * as vpath from "./vpath"; +import { compareStrings } from "./utils"; + +export interface IO { + newLine(): string; + useCaseSensitiveFileNames(): boolean; + getCurrentDirectory(): string; + getExecutingFilePath(): string; + getEnvironmentVariable(name: string): string; + args(): string[]; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + getAccessibleFileSystemEntries(path: string): FileSystemEntries; + getDirectories(path: string, options?: { recursive?: boolean, pattern?: RegExp, qualified?: boolean }): string[]; + getFiles(path: string, options?: { recursive?: boolean, pattern?: RegExp, qualified?: boolean }): string[]; + createDirectory(path: string): void; + readFile(path: string): string | undefined; + writeFile(path: string, contents: string): void; + deleteFile(fileName: string): void; + exit(exitCode?: number): void; +} + +export interface FileSystemEntries { + files: string[]; + directories: string[]; +} + +function createNodeIO(): IO { + const fs = require("fs"); + const os = require("os"); + const platform = os.platform(); + const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + const args = process.argv.slice(2); + const executingFilePath = vpath.combine(__dirname, "run.js"); + + return { + newLine: () => "\r\n", + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getCurrentDirectory: () => process.cwd(), + getExecutingFilePath: () => executingFilePath, + getEnvironmentVariable: name => process.env[name] as string || "", + args: () => args, + fileExists, + directoryExists, + getAccessibleFileSystemEntries, + getFiles, + getDirectories, + createDirectory, + readFile, + writeFile, + deleteFile, + exit + }; + + function isFileSystemCaseSensitive() { + if (platform === "win32" || platform === "win64") { + return false; + } + return !fileExists(__filename.toUpperCase()) + || !fileExists(__filename.toLowerCase()); + } + + function fileExists(path: string): boolean { + try { + return fs.statSync(path).isFile() as boolean; + } + catch (e) { + return false; + } + } + + function directoryExists(path: string): boolean { + try { + return fs.statSync(path).isDirectory(); + } + catch (e) { + return false; + } + } + + function getAccessibleFileSystemEntries(dirname: string): FileSystemEntries { + try { + const entries: string[] = fs.readdirSync(dirname || ".").sort(useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive); + const files: string[] = []; + const directories: string[] = []; + for (const entry of entries) { + if (entry === "." || entry === "..") continue; + const name = vpath.combine(dirname, entry); + try { + const stat = fs.statSync(name); + if (!stat) continue; + if (stat.isFile()) { + files.push(entry); + } + else if (stat.isDirectory()) { + directories.push(entry); + } + } + catch (e) { } + } + return { files, directories }; + } + catch (e) { + return { files: [], directories: [] }; + } + } + + function getEntries(dirname: string, options: { recursive?: boolean, pattern?: RegExp, qualified?: boolean, kind: "files" | "directories" }): string[] { + const results: string[] = []; + getEntriesWorker(dirname, options.qualified ? dirname : "", options, results); + if (options.recursive) results.sort(compareStrings); + return results; + } + + function getEntriesWorker(dirname: string, qualifiedname: string, options: { recursive?: boolean, pattern?: RegExp, kind: "files" | "directories" }, results: string[]) { + const entries = getAccessibleFileSystemEntries(dirname); + const names = entries[options.kind]; + for (const name of names) { + if (options.pattern && !options.pattern.test(name)) continue; + results.push(vpath.combine(qualifiedname, name)); + } + if (options.recursive) { + for (const name of entries.directories) { + getEntriesWorker(vpath.combine(dirname, name), vpath.combine(qualifiedname, name), options, results); + } + } + } + + function getFiles(path: string, options: { recursive?: boolean, pattern?: RegExp, qualified?: boolean } = {}) { + return getEntries(path, { ...options, kind: "files" }); + } + + function getDirectories(path: string, options: { recursive?: boolean, pattern?: RegExp, qualified?: boolean } = {}) { + return getEntries(path, { ...options, kind: "directories" }); + } + + function createDirectory(path: string) { + try { + fs.mkdirSync(path); + } + catch (e) { + if (e.code === "ENOENT") { + createDirectory(vpath.dirname(path)); + createDirectory(path); + } + else if (!directoryExists(path)) { + throw e; + } + } + } + + function readFile(name: string): string | undefined { + try { + const buffer = fs.readFileSync(name); + let len = buffer.length; + if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) { + // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js, + // flip all byte pairs and treat as little endian. + len &= ~1; // Round down to a multiple of 2 + for (let i = 0; i < len; i += 2) { + const temp = buffer[i]; + buffer[i] = buffer[i + 1]; + buffer[i + 1] = temp; + } + return buffer.toString("utf16le", 2); + } + if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { + // Little endian UTF-16 byte order mark detected + return buffer.toString("utf16le", 2); + } + if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + // UTF-8 byte order mark detected + return buffer.toString("utf8", 3); + } + // Default is UTF-8 with no byte order mark + return buffer.toString("utf8"); + } + catch (e) { + return undefined; + } + } + + function writeFile(name: string, contents: string) { + fs.writeFileSync(name, contents, "utf8"); + } + + function deleteFile(name: string) { + try { + fs.unlinkSync(name); + } + catch (e) { + } + } + + function exit(exitCode?: number) { + process.exit(exitCode); + } +} + +function createIO() { + return createNodeIO(); +} + +export const { + newLine, + useCaseSensitiveFileNames, + getCurrentDirectory, + getExecutingFilePath, + getEnvironmentVariable, + args, + getAccessibleFileSystemEntries, + directoryExists, + getDirectories, + createDirectory, + fileExists, + getFiles, + readFile, + writeFile, + deleteFile, + exit, +} = createIO(); \ No newline at end of file diff --git a/src/harness2/options.ts b/src/harness2/options.ts new file mode 100644 index 00000000000..3f323f76f83 --- /dev/null +++ b/src/harness2/options.ts @@ -0,0 +1,76 @@ +export interface ParsedArguments { + config?: string; + discover?: boolean; + [option: string]: string | boolean | undefined; +} + +interface Option { + name: string; + type: "string" | "boolean"; +} + +const options: Option[] = [ + { name: "config", type: "string" }, + { name: "discover", type: "boolean" } +]; + +const optionsMap = new Map(options.map(option => [option.name.toLowerCase(), option] as [string, Option])); + +/** + * Regular expression for matching command line arguments. + * Captures: + * 1. An optional `no-` prefix. + * 2. The name of the option. + * 3. An optional quote character (`'` or `"`, used for balancing quotes). + * 4. An optional inline value. + */ +const argRegExp = /^--(no-)?(\w+)(?:=(['"])?(.*)\3)?$/i; +const falseRegExp = /^false$/i; +export function parseCommandLine(args: string[]) { + const parsedArgs: ParsedArguments = { }; + for (let i = 0; i < args.length; i++) { + const match = argRegExp.exec(args[i]); + if (match) { + const [/*all*/, no, name, /*quote*/, value] = match; + const opt = optionsMap.get(name.toLowerCase()); + if (opt) { + switch (opt.type) { + case "string": + if (no) { + // `--no-` not supported on strings + break; + } + + let stringValue: string; + if (value) { + stringValue = value; + } + else if (i < args.length - 1) { + i++; + stringValue = args[i]; + } + else { + // string options require a value + break; + } + + parsedArgs[opt.name] = stringValue; + break; + + case "boolean": + let booleanValue = true; + if (value) { + booleanValue = !falseRegExp.test(value); + } + if (no) { + booleanValue = !booleanValue; + } + + parsedArgs[opt.name] = booleanValue; + break; + } + } + } + } + return parsedArgs; +} \ No newline at end of file diff --git a/src/harness2/run.ts b/src/harness2/run.ts new file mode 100644 index 00000000000..86308447d31 --- /dev/null +++ b/src/harness2/run.ts @@ -0,0 +1,187 @@ +/// +import * as io from "./io"; +import * as vpath from "./vpath"; +import * as ts from "./api"; +import { TestRunTask, TestRunnerKind } from "./runner"; +import { getTestConfig, TestConfig } from "./config"; +import { parseCommandLine } from "./options"; +import { getExecutionEnvironment, ExecutionEnvironment } from "./utils"; +import { CompilerRunner } from "./runners/compiler"; +import { install } from "source-map-support"; + +// enable source +install(); + +// start harness +main(); + +/** + * Get tasks that describe the test runners and test cases to use in this session. + */ +function getTestRunTasks(config: TestConfig): TestRunTask[] { + const runners: TestRunTask[] = []; + + // If `tasks` have been specified, add each task + if (config.tasks) { + for (const testRun of config.tasks) { + runners.push(testRun); + } + } + + // if `test` has been specified, add each test suite. + if (config.test) { + for (const option of config.test) { + switch (option) { + case "compiler": + runners.push({ runner: "conformance" }); + runners.push({ runner: "compiler" }); + // runners.push({ runner: "project" }); + break; + case "conformance": + runners.push({ runner: "conformance" }); + break; + // case "project": + // runners.push({ runner: "project" }); + // break; + // case "fourslash": + // runners.push({ runner: "fourslash" }); + // break; + // case "fourslash-shims": + // runners.push({ runner: "fourslash-shims" }); + // break; + // case "fourslash-shims-pp": + // runners.push({ runner: "fourslash-shims-pp" }); + // break; + // case "fourslash-server": + // runners.push({ runner: "fourslash-server" }); + // break; + // case "fourslash-generated": + // runners.push({ runner: "fourslash-generated" }); + // break; + // case "rwc": + // runners.push({ runner: "rwc" }); + // break; + // case "test262": + // runners.push({ runner: "test262" }); + // break; + } + } + } + + // if nothing has been specified, add defaults. + if (runners.length === 0) { + // compiler + runners.push({ runner: "conformance" }); + runners.push({ runner: "compiler" }); + + // TODO: project tests don't work in the browser yet + if (getExecutionEnvironment() !== ExecutionEnvironment.Browser) { + // runners.push({ runner: "project" }); + } + + // language services + // runners.push({ runner: "fourslash" }); + // runners.push({ runner: "fourslash-shims" }); + // runners.push({ runner: "fourslash-shims-pp" }); + // runners.push({ runner: "fourslash-server" }); + } + + return runners; +} + +/** + * Creates a test runner for the provided kind. + * @param kind The kind of runner to create + * @param config The test harness configuration + */ +function createRunner(kind: TestRunnerKind, config: TestConfig) { + switch (kind) { + case "conformance": return new CompilerRunner(kind, config); + case "compiler": return new CompilerRunner(kind, config); + } +} + +/** + * Divides the tasks in this session into one or more partitions to be run in parallel. + * Tests discovered are distributed between partitions as evenly as possible, with the exception + * that the last partition will also run the unit tests. + * @param taskConfigsFolder The folder into which test partition configuration files should be written. + * @param tasks The tasks for the session that should be partitioned. + */ +function discoverTests(config: TestConfig, tasks: TestRunTask[]) { + if (!config.taskConfigsFolder) throw new Error("Property 'taskConfigsFolder' not specified in config."); + if (!config.workerCount) throw new Error("Property 'workerCount' not specified in config."); + + // Create partitions + const partitions: TestConfig[] = []; + for (let i = 0; i < config.workerCount; i++) { + // Use the last worker to run unit tests. This may need to be changed in the future... + partitions.push({ light: config.light, tasks: [], runUnitTests: i === config.workerCount - 1 }); + } + + // Add tasks to each partition + for (const task of tasks) { + const runner = createRunner(task.runner, config); + const tests = runner.discover(); + const remainder = tests.length % config.workerCount; + const quotient = (tests.length - remainder) / config.workerCount; + let end = -1; + for (let i = 0; i < config.workerCount; i++) { + // We know that `tasks` is defined as we provided it above. + const tasks = partitions[i].tasks!; + const start = end + 1; + end = i < remainder ? start + quotient : start + quotient - 1; + tasks.push({ runner: runner.kind, tests: tests.slice(start, end) }); + } + } + + // Write each partition configuration. + for (let i = 0; i < config.workerCount; i++) { + io.writeFile(vpath.combine(config.taskConfigsFolder, `task-config${i}.json`), JSON.stringify(partitions[i])); + } +} + +/** + * Runs the provided tests. + * @param tasks The tasks for the session that should be run. + */ +function runTests(config: TestConfig, tasks: TestRunTask[]) { + // Set stack trace limit + if (config.stackTraceLimit === "full") { + Error.stackTraceLimit = Infinity; + } + else if (config.stackTraceLimit !== undefined) { + Error.stackTraceLimit = config.stackTraceLimit; + } + + // Enable debugging support + if (ts.Debug.isDebugging) { + ts.Debug.enableDebugInfo(); + } + + // Run tests + for (const task of tasks) { + const runner = createRunner(task.runner, config); + runner.run(task.tests); + } +} + +/** + * Main entrypoint for the harness. + */ +function main() { + const args = parseCommandLine(io.args()); + const config = getTestConfig(args); + const tasks = getTestRunTasks(config); + if (args.discover) { + discoverTests(config, tasks); + } + else { + runTests(config, tasks); + } + if (!config.runUnitTests) { + // patch `describe` to skip unit tests + // FIXME(rbuckton): We need to have a better way to handle this. + (global).describe = function () { }; + } +} \ No newline at end of file diff --git a/src/harness2/runner.ts b/src/harness2/runner.ts new file mode 100644 index 00000000000..88b54dd2087 --- /dev/null +++ b/src/harness2/runner.ts @@ -0,0 +1,74 @@ +import { TestConfig } from "./config"; + +export type TestRunnerKind = "conformance" | "compiler"; + +export interface TestRunTask { + /** + * The id of the associated runner. + */ + runner: TestRunnerKind; + + /** + * The ids of the test cases in the runner. + */ + tests?: string[]; +} + +export abstract class Runner { + public readonly kind: TKind; + public readonly config: TestConfig; + + constructor(kind: TKind, config: TestConfig) { + this.kind = kind; + this.config = config; + } + + /** + * Discover test cases for the runner. + */ + public abstract discover(): string[]; + + /** + * Setup the runner's tests so that they are ready to be executed by the harness. + * @param tests The tests for this run. + */ + public run(tests = this.discover()): void { + describe(`${this.kind} tests`, () => { + if (this.before !== Runner.prototype.before) before(() => this.before()); + if (this.beforeEach !== Runner.prototype.beforeEach) beforeEach(() => this.beforeEach()); + if (this.afterEach !== Runner.prototype.afterEach) afterEach(() => this.afterEach()); + if (this.after !== Runner.prototype.after) after(() => this.after()); + for (const test of tests) { + describe(`${this.kind} tests for ${test}`, () => { + this.runSuite(test); + }); + } + }); + } + + /** + * Override to perform initialization before any tests in the runner are executed. + */ + protected before(): void { } + + /** + * Override to perform initialization before each test in the runner is executed. + */ + protected beforeEach(): void { } + + /** + * Override to perform cleanup after any tests in the runner are executed. + */ + protected after(): void {} + + /** + * Override to perform cleanup after each test in the runner is executed. + */ + protected afterEach(): void { } + + /** + * Override to describe test suites and tests for a specific test case. + * @param id The id of the test case. + */ + protected abstract runSuite(id: string): void; +} \ No newline at end of file diff --git a/src/harness2/runners/compiler.ts b/src/harness2/runners/compiler.ts new file mode 100644 index 00000000000..1330471d2e8 --- /dev/null +++ b/src/harness2/runners/compiler.ts @@ -0,0 +1,299 @@ +import * as vpath from "../vpath"; +import * as io from "../io"; +import * as ts from "../api"; +import { Runner } from "../runner"; +import { TextDocument, isDeclarationDocument, isTypeScriptDocument } from "../documents"; +import { VirtualFileSystem } from "../vfs"; +import { parseTestCase, TestCaseOptions } from "../testCaseParser"; +import { compileFiles, CompilationResult, ParseConfigHost } from "../compiler"; +import { assert } from "chai"; +import { baseline } from "../baselines"; +import { formatDiagnostics, formatJavaScript, formatSourceMaps, formatTypes, formatSymbols, formatSourceMapData, formatModuleResolution } from "../formatters"; +import { isJsonFile, compareStrings } from "../utils"; + +export class CompilerRunner extends Runner<"conformance" | "compiler"> { + private _basePath: string | undefined; + + public get basePath() { + return this._basePath || (this._basePath = vpath.combine("tests/cases", this.kind)); + } + + // nee. enumerateTestFiles() + public discover(): string[] { + return io.getFiles(this.basePath, { recursive: true, pattern: /\.tsx?$/, qualified: true }); + } + + // nee. initializeTests() + protected runSuite(test: string): void { + let compilerTest: CompilerTest | undefined; + before(() => compilerTest = new CompilerTest(this, test)); + // it("errors", () => compilerTest && compilerTest.verifyDiagnostics()); + // it("module resolution", () => compilerTest && compilerTest.verifyModuleResolution()); + // it("output", () => compilerTest && compilerTest.verifyJavaScriptOutput()); + it("sourcemap record", () => compilerTest && compilerTest.verifySourceMapRecord()); + // it("sourcemap", () => compilerTest && compilerTest.verifySourceMapOutput()); + // it("types", () => compilerTest && compilerTest.verifyTypes()); + // it("symbols", () => compilerTest && compilerTest.verifySymbols()); + after(() => compilerTest = undefined); + } +} + +class CompilerTest { + private runner: CompilerRunner; + private basename: string; + private document: TextDocument; + private documents: TextDocument[]; + private configDocument: TextDocument | undefined; + private meta: Map; + private config: ts.ParsedCommandLine | undefined; + private options: TestCaseOptions; + private vfs: VirtualFileSystem; + private rootFiles: string[]; + private rootDocuments: TextDocument[]; + private nonRootDocuments: TextDocument[]; + private result: CompilationResult; + private declarationVfs: VirtualFileSystem | undefined; + private declarationRootFiles: string[] | undefined; + private declarationDocuments: TextDocument[] | undefined; + private declarationResult: CompilationResult | undefined; + private hasNonDeclarationFiles = false; + private typesAndSymbolsDocuments: TextDocument[] | undefined; + private typesAndSymbols: Map | undefined; + + constructor(runner: CompilerRunner, file: string) { + this.runner = runner; + this.basename = vpath.basename(file); + this.document = new TextDocument(file, io.readFile(file) || ""); + + const { documents, options, meta } = parseTestCase(this.document); + this.meta = meta; + this.options = options; + + if (options.useCaseSensitiveFileNames === undefined) options.useCaseSensitiveFileNames = true; + if (options.noTypesBaseline === undefined) options.noTypesBaseline = false; + if (options.noSymbolsBaseline === undefined) options.noSymbolsBaseline = false; + + // TODO: @baseUrl - May not be needed due to the use of the vfs. + // TODO: @baselineFile + + // FIXME(rbuckton): The old harness would effectively overwrite a previously + // declared file that shares the same name. This really should be an error. + this.vfs = VirtualFileSystem.createFromDocuments(this.options, documents, { overwrite: true }); + + const prepared = prepareDocuments(this.options, documents); + this.documents = prepared.documents; + this.configDocument = prepared.configDocument; + this.rootFiles = prepared.rootFiles; + this.rootDocuments = prepared.rootDocuments; + this.nonRootDocuments = prepared.nonRootDocuments; + this.hasNonDeclarationFiles = prepared.hasNonDeclarationFiles; + const noImplicitReferences = prepared.noImplicitReferences; + + if (this.configDocument) { + const { config } = ts.parseConfigFileTextToJson(this.configDocument.file, this.configDocument.text); + assert.isDefined(config); + const baseDir = vpath.dirname(this.configDocument.file); + const host = new ParseConfigHost(this.vfs); + this.config = ts.parseJsonConfigFileContent(config, host, baseDir, /*existingOptions*/ undefined, this.configDocument.file); + this.options = { ...this.config.options, ...this.options }; + } + else { + if (this.options.noResolve === undefined) this.options.noResolve = false; + } + + this.result = compileFiles(this.vfs, "/.ts", this.rootFiles, this.options); + + // check declaration files + if (this.hasNonDeclarationFiles && this.options.declaration && this.result.diagnostics.length === 0 && this.result.dts.size > 0) { + const declarationOptions = { ...this.options, declaration: false, noImplicitReferences }; + const declarationDocuments: TextDocument[] = []; + for (const document of documents) { + if (isDeclarationDocument(document) || !isTypeScriptDocument(document)) { + declarationDocuments.push(document); + } + const outputs = this.result.getInputsAndOutputs(document.file); + const dts = outputs && outputs.dts; + if (dts) { + declarationDocuments.push(dts); + } + } + + // FIXME(rbuckton): The old harness would effectively overwrite a previously + // declared file that shares the same name. This really should be an error. + this.declarationVfs = VirtualFileSystem.createFromDocuments(declarationOptions, declarationDocuments, { overwrite: true }); + + const prepared = prepareDocuments(declarationOptions, declarationDocuments); + this.declarationDocuments = prepared.documents; + this.declarationRootFiles = prepared.rootFiles; + this.declarationResult = compileFiles(this.declarationVfs, "/.ts", this.declarationRootFiles, declarationOptions); + } + + // walk types and symbols + if (this.result.diagnostics.length === 0 && (!this.options.noTypesBaseline || !this.options.noSymbolsBaseline)) { + // The full walker simulates the types that you would get from doing a full + // compile. The pull walker simulates the types you get when you just do + // a type query for a random node (like how the LS would do it). Most of the + // time, these will be the same. However, occasionally, they can be different. + // Specifically, when the compiler internally depends on symbol IDs to order + // things, then we may see different results because symbols can be created in a + // different order with 'pull' operations, and thus can produce slightly differing + // output. + // + // For example, with a full type check, we may see a type displayed as: number | string + // But with a pull type check, we may see it as: string | number + // + // These types are equivalent, but depend on what order the compiler observed + // certain parts of the program. + + const exclude = this.options.noTypesBaseline ? "types" : this.options.noSymbolsBaseline ? "symbols" : undefined; + this.typesAndSymbols = new Map(); + this.typesAndSymbolsDocuments = []; + for (const document of this.documents) { + if (!this.result.program.getSourceFile(document.file)) continue; + const typesAndSymbols = ts.getTypesAndSymbols(this.program, document.file, /*checked*/ true, exclude); + this.typesAndSymbols.set(document.file, typesAndSymbols); + this.typesAndSymbolsDocuments.push(document); + } + } + } + + private get isEmitSkipped() { + return this.options.noEmitOnError && this.result.diagnostics.length > 0; + } + + private get program() { + return this.result.program; + } + + public verifyDiagnostics(): void { + const hasContent = this.result.diagnostics.length > 0; + const content = hasContent ? formatDiagnostics(this.documents, this.result) : undefined; + baseline(vpath.chext(this.basename, ".errors.txt"), content); + } + + public verifyModuleResolution(): void { + if (!this.options.traceResolution) return; + const content = formatModuleResolution(this.result); + baseline(vpath.chext(this.basename, ".trace.json"), content); + } + + public verifySourceMapRecord(): void { + if (!this.options.sourceMap && !this.options.inlineSourceMap) return; + const content = this.isEmitSkipped ? undefined : formatSourceMapData(this.result); + baseline(vpath.chext(this.basename, ".sourcemap.txt"), content); + } + + public verifyJavaScriptOutput(): void { + if (!this.hasNonDeclarationFiles) return; + assert.isOk(this.options.noEmit || this.result.js.size || this.result.diagnostics.length, "Expected at least one js file to be emitted or at least one error to be created."); + assert.isOk(!this.options.declaration || this.result.diagnostics.length > 0 || this.result.dts.size === this.result.js.size, "There were no errors and declFiles generated did not match number of js files generated."); + const hasContent = this.result.js.size > 0 || this.result.dts.size > 0 || (this.declarationResult && this.declarationResult.diagnostics.length > 0); + let content: string | undefined; + if (hasContent) { + content = formatJavaScript( + this.document.file, + this.options.fullEmitPaths || false, + this.nonRootDocuments.concat(this.rootDocuments), // NOTE: The previous harness emits non-root documents before root documents. + this.result, + this.declarationDocuments && [...this.declarationDocuments, ...this.nonRootDocuments], + this.declarationResult + ); + } + baseline(vpath.chext(this.basename, ".js"), content); + } + + public verifySourceMapOutput(): void { + if (this.options.inlineSourceMap) { + assert.equal(this.result.maps.size, 0, "No sourcemap files should be generated if inlineSourceMaps was set."); + return; + } + + if (!this.options.sourceMap) return; + assert.equal(this.result.maps.size, this.result.js.size, "Number of sourcemap files should be same as js files."); + const hasContent = !this.isEmitSkipped && this.result.maps.size > 0; + const content = hasContent ? formatSourceMaps(this.options.fullEmitPaths || false, this.result) : undefined; + baseline(vpath.chext(this.basename, ".js.map"), content); + } + + public verifyTypes(): void { + if (this.options.noTypesBaseline || !this.typesAndSymbols || !this.typesAndSymbolsDocuments || this.result.diagnostics.length > 0) return; + const content = formatTypes(this.typesAndSymbolsDocuments, this.typesAndSymbols); + baseline(vpath.chext(this.basename, ".types"), content); + } + + public verifySymbols(): void { + if (this.options.noSymbolsBaseline || !this.typesAndSymbols || !this.typesAndSymbolsDocuments || this.result.diagnostics.length > 0) return; + const content = formatSymbols(this.typesAndSymbolsDocuments, this.typesAndSymbols); + baseline(vpath.chext(this.basename, ".symbols"), content); + } +} + +function prepareDocuments(options: TestCaseOptions, documents: TextDocument[]) { + const rootFiles: string[] = []; + const rootDocuments: TextDocument[] = []; + const nonRootDocuments: TextDocument[] = []; + const allDocuments: TextDocument[] = []; + let configDocument: TextDocument | undefined; + let hasNonDeclarationFiles = false; + + // FIXME(rbuckton): This is a mildly frustrating and esoteric feature of our test harness. + // We blindly assume that if the last document contains a call to `require` or a + // directive then we do not want implicit references. We instead need to + // find a way to be more explicit about this. + let lastDocument = documents[documents.length - 1]; + if (lastDocument && vpath.basename(lastDocument.file) === "tsconfig.json" && documents.length > 1) { + lastDocument = documents[documents.length - 2]; + } + + const noImplicitReferences = options.noImplicitReferences || /require\(/.test(lastDocument.text) || /reference\spath/.test(lastDocument.text); + + // FIXME(rbuckton): Odd ordering required to be compatible with existing harness + if (noImplicitReferences) { + allDocuments.push(lastDocument); + rootFiles.push(lastDocument.file); + rootDocuments.push(lastDocument); + } + + // Add documents + for (const document of documents) { + const basename = vpath.basename(document.file); + if (compareStrings(basename, "tsconfig.json", !options.useCaseSensitiveFileNames) === 0) { + if (!configDocument) { + configDocument = document; + } + } + else if (isJsonFile(basename)) { + allDocuments.push(document); + nonRootDocuments.push(document); + } + else { + if (!vpath.extname(document.file, { extensions: [".d.ts"] })) { + hasNonDeclarationFiles = true; + } + + // FIXME(rbuckton): Odd ordering required to be compatible with existing harness + if (noImplicitReferences) { + if (document.file === lastDocument.file) continue; + nonRootDocuments.push(document); + } + else { + rootFiles.push(document.file); + rootDocuments.push(document); + } + + allDocuments.push(document); + } + } + + if (options.includeBuiltFile) { + rootFiles.push(vpath.combine("/.ts", options.includeBuiltFile)); + } + + if (options.libFiles) { + for (const libFile of options.libFiles) { + rootFiles.push(vpath.combine("/.lib", libFile)); + } + } + + return { rootFiles, rootDocuments, nonRootDocuments, documents: allDocuments, configDocument, hasNonDeclarationFiles, noImplicitReferences }; +} \ No newline at end of file diff --git a/src/harness2/sourceMaps.ts b/src/harness2/sourceMaps.ts new file mode 100644 index 00000000000..d745c4fb062 --- /dev/null +++ b/src/harness2/sourceMaps.ts @@ -0,0 +1,164 @@ +export interface RawSourceMap { + version: number; + file: string; + sourceRoot?: string; + sources: string[]; + sourcesContent?: string[]; + names: string[]; + mappings: string; +} + +export interface Source { + file: string; + sourceIndex: number; + content?: string; +} + +export interface Mapping { + emittedLine: number; + emittedColumn: number; + source: Source; + sourceIndex: number; + sourceLine: number; + sourceColumn: number; + nameIndex?: number; + name?: string; +} + +const mappingRegExp = /([A-Za-z0-9+/]+),?|(;)|./g; +const sourceMappingURLRegExp = /^\/\/[#@]\s*sourceMappingURL\s*=\s*(.*?)\s*$/mig; +const dataURLRegExp = /^data:application\/json;base64,([a-z0-9+/=]+)$/i; + +export class SourceMap { + public readonly mapFile: string | undefined; + public readonly sourceMap: RawSourceMap; + public readonly version: number; + public readonly file: string; + public readonly sourceRoot: string | undefined; + public readonly sources: ReadonlyArray = []; + public readonly mappings: ReadonlyArray = []; + public readonly names: ReadonlyArray; + + private _emittedLineMappings: Mapping[][] = []; + private _sourceLineMappings: Mapping[][][] = []; + + constructor(mapFile: string | undefined, text: string) { + this.mapFile = mapFile; + this.sourceMap = JSON.parse(text); + this.version = this.sourceMap.version; + this.file = this.sourceMap.file; + this.sourceRoot = this.sourceMap.sourceRoot; + + // populate sources + const sources: Source[] = []; + for (let i = 0; i < this.sourceMap.sources.length; i++) { + const source: Source = { file: this.sourceMap.sources[i], sourceIndex: i }; + if (this.sourceMap.sourcesContent) { + source.content = this.sourceMap.sourcesContent[i]; + } + sources.push(source); + } + this.sources = sources; + + // populate names + this.names = this.sourceMap.names && this.sourceMap.names.slice() || []; + + // populate mappings + const mappings: Mapping[] = []; + let emittedLine = 0; + let emittedColumn = 0; + let sourceIndex = 0; + let sourceLine = 0; + let sourceColumn = 0; + let nameIndex = 0; + let match: RegExpExecArray | null; + while (match = mappingRegExp.exec(this.sourceMap.mappings)) { + if (match[1]) { + const segment = decodeVLQ(match[1]); + if (segment.length !== 1 && segment.length !== 4 && segment.length !== 5) { + throw new Error("Invalid VLQ"); + } + + emittedColumn += segment[0]; + if (segment.length >= 4) { + sourceIndex += segment[1]; + sourceLine += segment[2]; + sourceColumn += segment[3]; + } + + const mapping: Mapping = { emittedLine, emittedColumn, source: this.sources[sourceIndex], sourceIndex, sourceLine, sourceColumn }; + if (segment.length === 5) { + nameIndex += segment[4]; + mapping.nameIndex = nameIndex; + mapping.name = this.names[nameIndex]; + } + + mappings.push(mapping); + + const mappingsForEmittedLine = this._emittedLineMappings[mapping.emittedLine] || (this._emittedLineMappings[mapping.emittedLine] = []); + mappingsForEmittedLine.push(mapping); + + const mappingsForSource = this._sourceLineMappings[mapping.sourceIndex] || (this._sourceLineMappings[mapping.sourceIndex] = []); + const mappingsForSourceLine = mappingsForSource[mapping.sourceLine] || (mappingsForSource[mapping.sourceLine] = []); + mappingsForSourceLine.push(mapping); + } + else if (match[2]) { + emittedLine++; + emittedColumn = 0; + } + else { + throw new Error(`Unrecognized character '${match[0]}'.`); + } + } + + this.mappings = mappings; + } + + public static getUrl(text: string) { + let match: RegExpExecArray | null; + let lastMatch: RegExpExecArray | undefined; + while (match = sourceMappingURLRegExp.exec(text)) { + lastMatch = match; + } + return lastMatch ? lastMatch[1] : undefined; + } + + public static fromUrl(url: string) { + const match = dataURLRegExp.exec(url); + return match ? new SourceMap(/*mapFile*/ undefined, new Buffer(match[1], "base64").toString("utf8")) : undefined; + } + + public static fromSource(text: string) { + const url = this.getUrl(text); + return url && this.fromUrl(url); + } + + public getMappingsForEmittedLine(emittedLine: number): ReadonlyArray | undefined { + return this._emittedLineMappings[emittedLine]; + } + + public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): ReadonlyArray | undefined { + const mappingsForSource = this._sourceLineMappings[sourceIndex]; + return mappingsForSource && mappingsForSource[sourceLine]; + } +} + +const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +export function decodeVLQ(text: string) { + const vlq: number[] = []; + let shift = 0; + let value = 0; + for (let i = 0; i < text.length; i++) { + const currentByte = base64Chars.indexOf(text.charAt(i)); + value += (currentByte & 31) << shift; + if ((currentByte & 32) === 0) { + vlq.push(value & 1 ? -(value >>> 1) : value >>> 1); + shift = 0; + value = 0; + } + else { + shift += 5; + } + } + return vlq; +} diff --git a/src/harness2/testCaseParser.ts b/src/harness2/testCaseParser.ts new file mode 100644 index 00000000000..f10322e5ac5 --- /dev/null +++ b/src/harness2/testCaseParser.ts @@ -0,0 +1,159 @@ +import { TextDocument } from "./documents"; +import { TextWriter } from "./textWriter"; +import * as ts from "./api"; +import * as vpath from "./vpath"; +import { getLinesAndLineStarts } from "./utils"; + +const optionRegExp = /^\/{2}\s*@(\w+)\s*:\s*(.*?)\s*$/; + +export interface TestCaseOptions extends ts.CompilerOptions { + allowNonTsExtensions?: boolean; + baselineFile?: string; + currentDirectory?: string; + fullEmitPaths?: boolean; + includeBuiltFile?: string; + libFiles?: string[]; + noErrorTruncation?: boolean; + noImplicitReferences?: boolean; + noSymbolsBaseline?: boolean; + noTypesBaseline?: boolean; + useCaseSensitiveFileNames?: boolean; +} + +export interface TestCaseParseResult { + meta: Map; + options: TestCaseOptions; + documents: TextDocument[]; +} + +const testCaseOptions: ts.CommandLineOption[] = [ + { name: "allowNonTsExtensions", type: "boolean" }, + { name: "baselineFile", type: "string" }, + { name: "currentDirectory", type: "string" }, + { name: "fullEmitPaths", type: "boolean" }, // Emitted js baseline will print full paths for every output file + { name: "includeBuiltFile", type: "string" }, + { name: "libFiles", type: "list", element: { name: "libFiles", type: "string" } }, + { name: "suppressOutputPathCheck", type: "boolean" }, + { name: "noErrorTruncation", type: "boolean" }, + { name: "noImplicitReferences", type: "boolean" }, + { name: "noSymbolsBaseline", type: "boolean" }, + { name: "noTypesBaseline", type: "boolean" }, + { name: "useCaseSensitiveFileNames", type: "boolean" }, +]; + +let testCaseOptionsMap: Map | undefined; +function getCommandLineOption(name: string) { + if (!testCaseOptionsMap) { + testCaseOptionsMap = new Map(); + for (const opt of testCaseOptions) { + testCaseOptionsMap.set(opt.name.toLowerCase(), opt); + } + for (const opt of ts.optionDeclarations) { + testCaseOptionsMap.set(opt.name.toLowerCase(), opt); + } + } + return testCaseOptionsMap.get(name.toLowerCase()); +} + +function parseOption(opt: ts.CommandLineOption, value: string, diagnostics: ts.Diagnostic[]) { + switch (opt.type) { + case "boolean": + return value.toLowerCase() === "true"; + case "string": + return value; + case "number": + const number = parseInt(value, 10); + if (isNaN(number)) throw new Error(`Value must be a number, got: ${JSON.stringify(value)}`); + return number; + case "list": + return ts.parseListTypeOption(opt, value, diagnostics); + default: + return ts.parseCustomTypeOption(opt, value, diagnostics); + } +} + +export function parseTestCaseOptions(map: Map) { + const options: TestCaseOptions = { }; + const diagnostics: ts.Diagnostic[] = []; + map.forEach((value, key) => { + const opt = getCommandLineOption(key); + if (!opt) throw new Error(`Unknown compiler option '${key}'.`); + options[opt.name] = parseOption(opt, value, diagnostics); + if (diagnostics.length > 0) throw new Error(`Unrecognized value '${value}' for compiler option '${key}'.`); + }); + return options; +} + +function isDocumentMetadata(key: string) { + return key === "symlink"; +} + +export function parseTestCase(document: TextDocument): TestCaseParseResult { + const text = document.text; + const meta = new Map(); + const documents: TextDocument[] = []; + const dirname = vpath.dirname(document.file); + // NOTE: Old compiler is inconsistent as it uses `\n` instead of `\r\n` here. + const writer = new TextWriter(/*text*/ undefined, { eol: "\n" }); + let documentName: string | undefined; + let documentMeta: Map | undefined; + let preserveLines = false; + let needsSeparator = false; + const { lines, lineStarts } = getLinesAndLineStarts(text); + for (let i = 0; i < lineStarts.length; i++) { + const line = lines[i]; + const match = optionRegExp.exec(line); + if (match) { + const key = match[1].trim(); + const keyLower = key.toLowerCase(); + const value = match[2].trim(); + if (keyLower === "filename") { + // add previous document + if (documentName && documentMeta) { + documents.push(new TextDocument(documentName, writer.toString(), documentMeta)); + writer.clear(); + needsSeparator = false; + } + + // start new document + documentName = vpath.combine(dirname, value); + documentMeta = new Map(); + documentMeta.set("filename", documentName); + } + else if (keyLower === "preservelines") { + preserveLines = value.toLowerCase() === "true"; + } + else if (isDocumentMetadata(keyLower)) { + if (documentMeta) { + documentMeta.set(key, value); + } + } + else { + meta.set(key, value); + } + } + else { + if (needsSeparator) { + writer.writeln(); + } + if (preserveLines) { + const lineStart = lineStarts[i]; + const lineEnd = i < lineStarts.length - 1 ? lineStarts[i + 1] : text.length; + writer.write(text.slice(lineStart, lineEnd)); + if (needsSeparator && writer.size > 0) { + needsSeparator = false; + } + } + else { + writer.write(line); + if (!needsSeparator && writer.size > 0) { + needsSeparator = true; + } + } + } + } + + // Add remaining document + documents.push(new TextDocument(documentName || document.file, writer.toString(), documentMeta || meta)); + return { meta, options: parseTestCaseOptions(meta), documents }; +} \ No newline at end of file diff --git a/src/harness2/textWriter.ts b/src/harness2/textWriter.ts new file mode 100644 index 00000000000..158a630da26 --- /dev/null +++ b/src/harness2/textWriter.ts @@ -0,0 +1,60 @@ +export class TextWriter { + private text: string; + private eol: string; + private indents = ["", " "]; + private indentDepth = 0; + private indentRequested = true; + + constructor(text?: string, options?: { eol?: string }) { + this.text = text || ""; + this.eol = options && options.eol || "\r\n"; + } + + public get size() { + return this.text.length; + } + + public write(text?: string) { + if (text) { + this.writeIndent(); + this.text += text; + } + return this; + } + + public writeln(text?: string) { + this.write(text); + this.text += this.eol; + this.indentRequested = true; + return this; + } + + public increaseIndent() { + this.indentDepth++; + return this; + } + + public decreaseIndent() { + this.indentDepth = Math.max(0, this.indentDepth - 1); + return this; + } + + public toString() { + return this.text; + } + + public clear() { + this.text = ""; + this.indentRequested = true; + this.indentDepth = 0; + } + + private writeIndent() { + if (!this.indentRequested) return; + while (this.indents.length <= this.indentDepth) { + this.indents[this.indents.length] = this.indents[this.indents.length - 1] + this.indents[1]; + } + if (this.indentDepth) this.text += this.indents[this.indentDepth]; + this.indentRequested = false; + } +} \ No newline at end of file diff --git a/src/harness2/tsconfig.json b/src/harness2/tsconfig.json new file mode 100644 index 00000000000..d043857e68d --- /dev/null +++ b/src/harness2/tsconfig.json @@ -0,0 +1,43 @@ +{ + "extends": "../tsconfig-base", + "compilerOptions": { + "removeComments": false, + "outDir": "../../built/harness/", + "module": "commonjs", + "declaration": false, + "types": [ + "node", + "mocha", + "chai" + ], + "lib": [ + "es6", + "scripthost" + ], + "strict": true + }, + "files": [ + "utils.ts", + "collections.ts", + "vpath.ts", + "vfs.ts", + "io.ts", + "options.ts", + "documents.ts", + "testCaseParser.ts", + "textWriter.ts", + "sourceMaps.ts", + "baselines.ts", + "compiler.ts", + "formatters/index.ts", + "formatters/diagnostics.ts", + "formatters/javaScript.ts", + "formatters/moduleResolution.ts", + "formatters/sourceMaps.ts", + "formatters/types.ts", + "formatters/symbols.ts", + "runner.ts", + "runners/compiler.ts", + "run.ts" + ] +} diff --git a/src/harness2/utils.ts b/src/harness2/utils.ts new file mode 100644 index 00000000000..99b50400f08 --- /dev/null +++ b/src/harness2/utils.ts @@ -0,0 +1,320 @@ +import * as vpath from "./vpath"; + +declare var window: any; + +export const enum ExecutionEnvironment { + Node, + Browser, +} + +export function getExecutionEnvironment() { + if (typeof window !== "undefined") { + return ExecutionEnvironment.Browser; + } + else { + return ExecutionEnvironment.Node; + } +} + +let executionDirectory: string | undefined; +export function getExecutionDirectory() { + return executionDirectory || (executionDirectory = vpath.resolve(__dirname, "../../")); +} + +export function getBuiltDirectory() { + return vpath.combine(getExecutionDirectory(), "built/local"); +} + +export function getLibFilesDirectory() { + return vpath.combine(getExecutionDirectory(), "tests/lib"); +} + +// +// String Utilities +// + +export function toUtf8(text: string): string { + return new Buffer(text).toString("utf8"); +} + +export function compareValues(a: T, b: T) { + if (a === b) return 0; + if (a === undefined || a === null) return -1; // tslint:disable-line:no-null-keyword + if (b === undefined || b === null) return +1; // tslint:disable-line:no-null-keyword + return a < b ? -1 : a > b ? +1 : 0; +} + +const caseInsensitiveComparisonCollator = typeof Intl === "object" ? new Intl.Collator(/*locales*/ undefined, { usage: "sort", sensitivity: "accent" }) : undefined; +const caseSensitiveComparisonCollator = typeof Intl === "object" ? new Intl.Collator(/*locales*/ undefined, { usage: "sort", sensitivity: "variant" }) : undefined; + +export function compareStrings(a: string | undefined, b: string | undefined, ignoreCase?: boolean) { + if (a === b) return 0; + if (a === undefined) return -1; + if (b === undefined) return +1; + const collator = ignoreCase ? caseInsensitiveComparisonCollator : caseSensitiveComparisonCollator; + if (collator) { + return collator.compare(a, b); + } + else if (ignoreCase) { + a = a.toUpperCase(); + b = b.toUpperCase(); + } + return a < b ? -1 : a > b ? +1 : 0; +} + +export namespace compareStrings { + export function caseSensitive(a: string | undefined, b: string | undefined) { + return compareStrings(a, b, /*ignoreCase*/ false); + } + export function caseInsensitive(a: string | undefined, b: string | undefined) { + return compareStrings(a, b, /*ignoreCase*/ true); + } +} + +export function padLeft(text: string, size: number, ch = " ") { + while (text.length < size) text = ch + text; + return text; +} + +export function padRight(text: string, size: number, ch = " ") { + while (text.length < size) text += ch; + return text; +} + +export function repeatString(text: string, count: number) { + let result = ""; + for (let i = 0; i < count; i++) { + result += text; + } + return result; +} + +function splitLinesWorker(text: string, lines: string[] | undefined, removeEmptyElementsOrLineStarts: boolean | number[] | undefined) { + const lineStarts = typeof removeEmptyElementsOrLineStarts !== "boolean" ? removeEmptyElementsOrLineStarts : undefined; + const removeEmptyElements = typeof removeEmptyElementsOrLineStarts === "boolean" ? removeEmptyElementsOrLineStarts : undefined; + let pos = 0; + let end = 0; + let lineStart = 0; + let nonWhiteSpace = false; + while (pos < text.length) { + const ch = text.charCodeAt(pos); + end = pos; + pos++; + switch (ch) { + // LineTerminator + case 0x000d: // carriage return + if (pos < text.length && text.charCodeAt(pos) === 0x000a) pos++; + // falls through + case 0x000a: // line feed + case 0x2028: // line separator + case 0x2029: // paragraph separator + if (lineStarts) { + lineStarts.push(lineStart); + } + if (lines && (!removeEmptyElements || nonWhiteSpace)) { + lines.push(text.slice(lineStart, end)); + } + lineStart = pos; + nonWhiteSpace = false; + break; + + // WhiteSpace + case 0x0009: // tab + case 0x000b: // vertical tab + case 0x000c: // form feed + case 0x0020: // space + case 0x00a0: // no-break space + case 0xfeff: // zero width no-break space + case 0x1680: // ogham space mark + case 0x2000: // en quad + case 0x2001: // em quad + case 0x2002: // en space + case 0x2003: // em space + case 0x2004: // three-per-em space + case 0x2005: // four-per-em space + case 0x2006: // six-per-em space + case 0x2007: // figure space + case 0x2008: // punctuation space + case 0x2009: // thin space + case 0x200a: // hair space + case 0x202f: // narrow no-break space + case 0x205f: // medium mathematical space + case 0x3000: // ideographic space + case 0x0085: // next-line (not strictly per spec, but used by the compiler) + break; + + default: + nonWhiteSpace = true; + break; + } + } + if (lineStarts) { + lineStarts.push(lineStart); + } + if (lines && (!removeEmptyElements || nonWhiteSpace)) { + lines.push(text.slice(lineStart, text.length)); + } +} + +export function getLinesAndLineStarts(text: string) { + const lines: string[] = []; + const lineStarts: number[] = []; + splitLinesWorker(text, lines, lineStarts); + return { lines, lineStarts }; +} + +export function splitLines(text: string, removeEmptyElements?: boolean): string[] { + const lines: string[] = []; + splitLinesWorker(text, lines, removeEmptyElements); + return lines; +} + +export function computeLineStarts(text: string): number[] { + const lineStarts: number[] = []; + splitLinesWorker(text, /*lines*/ undefined, lineStarts); + return lineStarts; +} + +const commentRegExp = /(['"])(?:(?!\1).|\\\1)*\1|(\/\*.*?\*\/|\/\/.*?$)/gm; +export function removeComments(text: string) { + return text.replace(commentRegExp, (match, quote) => { + return quote ? match : ""; + }); +} + +const testPathPrefixRegExp = /\/\.(test|ts|lib)\//g; +export function removeTestPathPrefixes(text: string) { + return text.replace(testPathPrefixRegExp, ""); +} + +export function stripBOM(text: string) { + if (text.length >= 2) { + const ch0 = text.charCodeAt(0); + const ch1 = text.charCodeAt(1); + if ((ch0 === 0xff && ch1 === 0xfe) || + (ch0 === 0xfe && ch1 === 0xff)) { + return text.slice(2); + } + if (text.length >= 3 && ch0 === 0xef && ch1 === 0xbb && text.charCodeAt(2) === 0xbf) { + return text.slice(3); + } + } + return text; +} + +// +// Function Utilities +// + +export function identity(value: T) { + return value; +} + +// +// Array Utilities +// + +export function insertAt(array: T[], index: number, value: T) { + if (index === 0) { + array.unshift(value); + } + else if (index === array.length) { + array.push(value); + } + else { + for (let i = array.length; i > index; i--) { + array[i] = array[i - 1]; + } + array[index] = value; + } +} + +export function removeAt(array: T[], index: number): T | undefined { + const value = index < array.length ? array[index] : undefined; + for (let i = index; i < array.length - 1; i++) { + array[i] = array[i + 1]; + } + array.length--; + return value; +} + +export function stableSort(array: T[], comparer: (x: T, y: T) => number = compareValues) { + return array + .map((_, i) => i) // create array of indices + .sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)) // sort indices by value then position + .map(i => array[i]); // get sorted array +} + +export function binarySearch(array: ReadonlyArray, value: T, comparer?: (a: T, b: T) => number, offset?: number): number { + return keyedBinarySearch(array, value, identity, comparer, offset); +} + +export function keyedBinarySearch(array: ReadonlyArray, key: K, keySelector: (value: T) => K, keyComparer: (v1: K, v2: K) => number = compareValues, offset?: number): number { + if (array.length === 0) { + return -1; + } + + let low = offset || 0; + let high = array.length - 1; + + while (low <= high) { + const middle = low + ((high - low) >> 1); + const midKey = keySelector(array[middle]); + if (keyComparer(midKey, key) === 0) { + return middle; + } + else if (keyComparer(midKey, key) > 0) { + high = middle - 1; + } + else { + low = middle + 1; + } + } + + return ~low; +} + +// +// RegExp Utilities +// + +const reservedCharacterRegExp = /[^\w\s\/]/g; +export function escapeRegExp(pattern: string) { + return pattern.replace(reservedCharacterRegExp, match => "\\" + match); +} + +// +// Path Utilities +// + +export function isTypeScriptFile(path: string) { + return path.endsWith(".ts") + || path.endsWith(".tsx"); +} + +export function isJavaScriptFile(path: string) { + return path.endsWith(".js") + || path.endsWith(".jsx"); +} + +export function isDeclarationFile(path: string) { + return path.endsWith(".d.ts"); +} + +export function isSourceMapFile(path: string) { + return path.endsWith(".map"); +} + +export function isJsonFile(path: string) { + return path.endsWith(".json"); +} + +export function isDefaultLibraryFile(path: string) { + return isDeclarationFile(path) + && vpath.basename(path).startsWith("lib."); +} + +export function isBuiltFile(path: string) { + return path.includes("built/local/") + || path.includes("built/harness/"); +} \ No newline at end of file diff --git a/src/harness2/vfs.ts b/src/harness2/vfs.ts new file mode 100644 index 00000000000..7f5157855ce --- /dev/null +++ b/src/harness2/vfs.ts @@ -0,0 +1,917 @@ +import { EventEmitter } from "events"; +import { compareStrings, getBuiltDirectory, getLibFilesDirectory, escapeRegExp, identity } from "./utils"; +import { KeyedCollection, Metadata } from "./collections"; +import { assert } from "chai"; +import * as vpath from "./vpath"; +import * as io from "./io"; +import { TestCaseOptions } from "./testCaseParser"; +import { TextDocument } from "./documents"; + +export interface PathMappings { + [path: string]: string; +} + +export interface FileSystemResolver { + getEntries(dir: VirtualDirectory): { files: string[], directories: string[] }; + getContent(file: VirtualFile): string | undefined; +} + +function createMapper(ignoreCase: boolean, map: PathMappings | undefined) { + if (!map) return identity; + const roots = Object.keys(map); + const patterns = roots.map(root => createPattern(root, ignoreCase)); + return function (path: string) { + for (let i = 0; i < patterns.length; i++) { + const match = patterns[i].exec(path); + if (match) { + const prefix = path.slice(0, match.index); + const suffix = path.slice(match.index + match[0].length); + return vpath.combine(prefix, map[roots[i]], suffix); + } + } + return path; + }; +} + +function createPattern(path: string, ignoreCase: boolean) { + path = vpath.normalizeSlashes(path); + const components = vpath.parse(path); + let pattern = ""; + for (let i = 1; i < components.length; i++) { + const component = components[i]; + if (pattern) pattern += "/"; + pattern += escapeRegExp(component); + } + pattern = (components[0] ? "^" + escapeRegExp(components[0]) : "/") + pattern + "(/|$)"; + return new RegExp(pattern, ignoreCase ? "i" : ""); +} + +export function createResolver(io: io.IO, map?: PathMappings): FileSystemResolver { + const mapper = createMapper(!io.useCaseSensitiveFileNames(), map); + return { + getEntries(dir) { + return io.getAccessibleFileSystemEntries(mapper(dir.path)); + }, + getContent(file) { + return io.readFile(mapper(file.path)); + } + }; +} + +/** + * Represents a file system entry in a virtual file system. + */ +export abstract class VirtualFileSystemEntry extends EventEmitter { + private _readOnly = false; + private _path: string | undefined; + private _metadata: Metadata | undefined; + + /** + * Gets the file system to which this entry belongs. + */ + public readonly fileSystem: VirtualFileSystem; + + /** + * Gets the container for this file system entry. + */ + public readonly parent: VirtualFileSystemContainer; + + /** + * Gets the name of this file system entry. + */ + public readonly name: string; + + constructor(parent: VirtualFileSystemContainer | undefined, name: string) { + super(); + + if (this instanceof VirtualFileSystem) { + this.parent = this.fileSystem = this; + } + else if (parent instanceof VirtualDirectoryRoot) { + this.parent = this.fileSystem = parent.fileSystem; + } + else if (parent) { + this.parent = parent; + this.fileSystem = parent.fileSystem; + } + else { + throw new TypeError("Argument not optional: parent"); + } + + this.name = name; + } + + /** + * Gets the file system entry that this entry shadows. + */ + public abstract get shadowRoot(): VirtualFileSystemEntry | undefined; + + public get metadata(): Metadata { + if (!this._metadata) { + this._metadata = new Metadata(this.shadowRoot ? this.shadowRoot.metadata : undefined); + } + return this._metadata; + } + + public get isReadOnly(): boolean { + return this._readOnly; + } + + public get path(): string { + return this._path || (this._path = vpath.combine(this.parent.path, this.name)); + } + + public get relative(): string { + return this.relativeTo(this.fileSystem.currentDirectory); + } + + public get exists(): boolean { + return this.parent.exists + && this.parent.getEntry(this.name) as VirtualFileSystemEntry === this; + } + + public makeReadOnly(): void { + this.makeReadOnlyCore(); + this._readOnly = true; + } + + public relativeTo(other: string | VirtualFileSystemEntry) { + if (other) { + const otherPath = typeof other === "string" ? other : other.path; + const ignoreCase = !this.fileSystem.useCaseSensitiveFileNames; + return vpath.relative(otherPath, this.path, ignoreCase); + } + return this.path; + } + + /** + * Creates a file system entry that shadows this file system entry. + * @param parent The container for the shadowed entry. + */ + public abstract shadow(parent: VirtualFileSystemContainer): VirtualFileSystemEntry; + + protected abstract makeReadOnlyCore(): void; + + protected writePreamble(): void { + if (this._readOnly) throw new Error("Cannot modify a frozen entry."); + } + + protected shadowPreamble(parent: VirtualFileSystemContainer): void { + let fileSystem: VirtualFileSystem | undefined = this.fileSystem; + while (fileSystem) { + if (parent.fileSystem === fileSystem) throw new Error("Cannot create shadow for parent in the same file system."); + fileSystem = fileSystem.shadowRoot; + } + } +} + +export abstract class VirtualFileSystemContainer extends VirtualFileSystemEntry { + public abstract get shadowRoot(): VirtualFileSystemContainer | undefined; + + public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile[]; + public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory[]; + public getEntries(options?: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): (VirtualFile | VirtualDirectory)[]; + public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): (VirtualFile | VirtualDirectory)[] { + const results: (VirtualFile | VirtualDirectory)[] = []; + if (options.recursive) { + this.getOwnEntries().forEach(entry => { + if (entry instanceof VirtualFile) { + if (isMatch(entry, options)) { + results.push(entry); + } + } + else if (entry instanceof VirtualDirectory) { + if (isMatch(entry, options)) { + results.push(entry); + } + for (const child of entry.getEntries(options)) { + results.push(child); + } + } + }); + } + else { + this.getOwnEntries().forEach(entry => { + if (isMatch(entry, options)) { + results.push(entry); + } + }); + } + return results; + } + + public getDirectories(options: { recursive?: boolean, pattern?: RegExp } = {}): VirtualDirectory[] { + return this.getEntries({ kind: "directory", ...options }); + } + + public getFiles(options: { recursive?: boolean, pattern?: RegExp } = {}): VirtualFile[] { + return this.getEntries({ kind: "file", ...options }); + } + + public getEntryNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): string[] { + return this.getEntries(options).map(entry => + options && options.qualified ? entry.path : + options && options.recursive ? entry.relativeTo(this) : + entry.name); + } + + public getDirectoryNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp } = {}): string[] { + return this.getEntryNames({ kind: "directory", ...options }); + } + + public getFileNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp } = {}): string[] { + return this.getEntryNames({ kind: "file", ...options }); + } + + public abstract getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + public abstract getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + public abstract getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + + public getDirectory(path: string, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualDirectory | undefined { + return this.getEntry(path, { kind: "directory", ...options }); + } + + public getFile(path: string, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualFile | undefined { + return this.getEntry(path, { kind: "file", ...options }); + } + + protected abstract getOwnEntries(): KeyedCollection; +} + +export interface VirtualFileSystemContainer { + on(name: "childAdded", handler: (entry: VirtualFile | VirtualDirectory) => void): this; + on(name: "childRemoved", handler: (entry: VirtualFile | VirtualDirectory) => void): this; +} + +export class VirtualFileSystem extends VirtualFileSystemContainer { + private static _builtLocal: VirtualFileSystem | undefined; + private static _builtLocalCI: VirtualFileSystem | undefined; + private static _builtLocalCS: VirtualFileSystem | undefined; + + private _root: VirtualDirectoryRoot; + private _useCaseSensitiveFileNames: boolean; + private _currentDirectory: string; + private _shadowRoot: VirtualFileSystem | undefined; + + constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) { + super(/*parent*/ undefined, ""); + this._currentDirectory = currentDirectory.replace(/\\/g, "/"); + this._useCaseSensitiveFileNames = useCaseSensitiveFileNames; + } + + public get shadowRoot(): VirtualFileSystem | undefined { + return this._shadowRoot; + } + + public get useCaseSensitiveFileNames() { + return this._useCaseSensitiveFileNames; + } + + public get currentDirectory() { + return this._currentDirectory; + } + + public get path() { + return ""; + } + + public get relative() { + return ""; + } + + public get exists() { + return true; + } + + private get root() { + if (this._root === undefined) { + if (this._shadowRoot) { + this._root = this._shadowRoot.root.shadow(this); + } + else { + this._root = new VirtualDirectoryRoot(this); + } + if (this.isReadOnly) this._root.makeReadOnly(); + } + return this._root; + } + + public static getBuiltLocal(useCaseSensitiveFileNames: boolean = io.useCaseSensitiveFileNames()): VirtualFileSystem { + let vfs = useCaseSensitiveFileNames ? this._builtLocalCS : this._builtLocalCI; + if (!vfs) { + vfs = this._builtLocal; + if (!vfs) { + const resolver = createResolver(io, { + "/.ts": getBuiltDirectory(), + "/.lib": getLibFilesDirectory() + }); + vfs = new VirtualFileSystem("/", io.useCaseSensitiveFileNames()); + vfs.addDirectory(".ts", resolver); + vfs.addDirectory(".lib", resolver); + vfs.makeReadOnly(); + this._builtLocal = vfs; + } + if (vfs._useCaseSensitiveFileNames !== useCaseSensitiveFileNames) { + vfs = vfs.shadow(); + vfs._useCaseSensitiveFileNames = useCaseSensitiveFileNames; + vfs.makeReadOnly(); + } + return useCaseSensitiveFileNames + ? this._builtLocalCS = vfs + : this._builtLocalCI = vfs; + } + return vfs; + } + + public static createFromOptions(compilerOptions: TestCaseOptions) { + const vfs = this.getBuiltLocal(compilerOptions.useCaseSensitiveFileNames).shadow(); + vfs.addDirectory("/.test"); + if (compilerOptions.currentDirectory) { + const currentDirectory = vpath.resolve("/.test", compilerOptions.currentDirectory); + vfs.addDirectory(currentDirectory); + vfs.changeDirectory(currentDirectory); + } + else { + vfs.changeDirectory("/.test"); + } + return vfs; + } + + public static createFromDocuments(compilerOptions: TestCaseOptions, documents: TextDocument[], options?: { overwrite?: boolean }) { + const vfs = this.createFromOptions(compilerOptions); + for (const document of documents) { + const file = vfs.addFile(document.file, document.text, options)!; + assert.isDefined(file, `Failed to add file: '${document.file}'`); + file.metadata.set("document", document); + // Add symlinks + const symlink = document.meta.get("symlink"); + if (file && symlink) { + for (const link of symlink.split(",")) { + const symlink = vfs.addSymlink(vpath.resolve(vfs.currentDirectory, link.trim()), file)!; + assert.isDefined(symlink, `Failed to symlink: '${link}'`); + symlink.metadata.set("document", document); + } + } + } + return vfs; + } + + public changeDirectory(path: string) { + this.writePreamble(); + if (path) { + this._currentDirectory = vpath.resolve(this._currentDirectory, path); + } + } + + public addDirectory(path: string, resolver?: FileSystemResolver) { + return this.root.addDirectory(vpath.resolve(this.currentDirectory, path), resolver); + } + + public addFile(path: string, content?: FileSystemResolver["getContent"] | string, options?: { overwrite?: boolean }) { + return this.root.addFile(vpath.resolve(this.currentDirectory, path), content, options); + } + + public addSymlink(path: string, target: VirtualFile): VirtualFileSymlink | undefined; + public addSymlink(path: string, target: VirtualDirectory): VirtualDirectorySymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory) { + if (typeof target === "string") target = vpath.resolve(this.currentDirectory, target); + return this.root.addSymlink(vpath.resolve(this.currentDirectory, path), target); + } + + public removeDirectory(path: string): boolean { + return this.root.removeDirectory(vpath.resolve(this.currentDirectory, path)); + } + + public removeFile(path: string): boolean { + return this.root.removeFile(vpath.resolve(this.currentDirectory, path)); + } + + public directoryExists(path: string) { + return this.getEntry(path) instanceof VirtualDirectory; + } + + public fileExists(path: string) { + return this.getEntry(path) instanceof VirtualFile; + } + + public sameName(a: string, b: string) { + return compareStrings(a, b, !this.useCaseSensitiveFileNames) === 0; + } + + public getRealEntry(entry: VirtualDirectory): VirtualDirectory | undefined; + public getRealEntry(entry: VirtualFile): VirtualFile | undefined; + public getRealEntry(entry: VirtualFile | VirtualDirectory): VirtualFile | VirtualDirectory | undefined; + public getRealEntry(entry: VirtualFile | VirtualDirectory): VirtualFile | VirtualDirectory | undefined { + if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) { + return findTarget(this, entry.target); + } + return entry; + } + + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }) { + return this.root.getEntry(vpath.resolve(this.currentDirectory, path), options); + } + + public getFile(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualFile | undefined { + return this.root.getFile(vpath.resolve(this.currentDirectory, path), options); + } + + public getDirectory(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualDirectory | undefined { + return this.root.getDirectory(vpath.resolve(this.currentDirectory, path), options); + } + + public getAccessibleFileSystemEntries(path: string) { + const entry = this.getEntry(path); + if (entry instanceof VirtualDirectory) { + return { + files: entry.getFiles().map(f => f.name), + directories: entry.getDirectories().map(d => d.name) + }; + } + return { files: [], directories: [] }; + } + + public shadow(): VirtualFileSystem { + const fs = new VirtualFileSystem(this.currentDirectory, this.useCaseSensitiveFileNames); + fs._shadowRoot = this; + return fs; + } + + protected makeReadOnlyCore() { + this.root.makeReadOnly(); + } + + protected getOwnEntries() { + return this.root["getOwnEntries"](); + } +} + +export class VirtualDirectory extends VirtualFileSystemContainer { + protected _shadowRoot: VirtualDirectory | undefined; + + private _entries: KeyedCollection | undefined; + private _resolver: FileSystemResolver | undefined; + + constructor(parent: VirtualFileSystemContainer, name: string, resolver?: FileSystemResolver) { + super(parent, name); + this._entries = undefined; + this._resolver = resolver; + this._shadowRoot = undefined; + } + + public get shadowRoot(): VirtualDirectory | undefined { + return this._shadowRoot; + } + + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined { + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ false); + return directory && directory.getOwnEntry(components[components.length - 1], options); + } + + public addDirectory(path: string, resolver?: FileSystemResolver): VirtualDirectory | undefined { + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ true); + return directory && directory.addOwnDirectory(components[components.length - 1], resolver); + } + + public addFile(path: string, content?: FileSystemResolver["getContent"] | string | undefined, options?: { overwrite?: boolean }): VirtualFile | undefined { + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ true); + return directory && directory.addOwnFile(components[components.length - 1], content, options); + } + + public addSymlink(path: string, target: VirtualFile): VirtualFileSymlink | undefined; + public addSymlink(path: string, target: VirtualDirectory): VirtualDirectorySymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined { + this.writePreamble(); + const targetEntry = typeof target === "string" ? this.fileSystem.getEntry(vpath.resolve(this.path, target)) : target; + if (targetEntry === undefined) return undefined; + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ true); + return directory && directory.addOwnSymlink(components[components.length - 1], targetEntry); + } + + public removeDirectory(path: string): boolean { + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ false); + return directory ? directory.removeOwnDirectory(components[components.length - 1]) : false; + } + + public removeFile(path: string): boolean { + this.writePreamble(); + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ false); + return directory ? directory.removeOwnFile(components[components.length - 1]) : false; + } + + public shadow(parent: VirtualFileSystemContainer): VirtualDirectory { + this.shadowPreamble(parent); + const shadow = new VirtualDirectory(parent, this.name); + shadow._shadowRoot = this; + return shadow; + } + + protected makeReadOnlyCore(): void { + if (this._entries) { + this._entries.forEach(entry => entry.makeReadOnly()); + } + } + + protected getOwnEntries() { + if (!this._entries) { + const resolver = this._resolver; + const entries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive); + this._resolver = undefined; + if (resolver) { + const { files, directories } = resolver.getEntries(this); + for (const dir of directories) { + const vdir = new VirtualDirectory(this, dir, resolver); + if (this.isReadOnly) vdir.makeReadOnly(); + entries.set(vdir.name, vdir); + } + for (const file of files) { + const vfile = new VirtualFile(this, file, file => resolver.getContent(file)); + if (this.isReadOnly) vfile.makeReadOnly(); + entries.set(vfile.name, vfile); + } + } + else if (this._shadowRoot) { + this._shadowRoot.getOwnEntries().forEach(entry => { + const clone = (entry).shadow(this); + if (this.isReadOnly) clone.makeReadOnly(); + entries.set(clone.name, clone); + }); + } + this._entries = entries; + } + return this._entries; + } + + private parsePath(path: string) { + if (this instanceof VirtualDirectoryRoot) path = vpath.resolve(this.fileSystem.currentDirectory, path); + return vpath.parse(vpath.normalize(path)); + } + + private walkContainers(components: string[], create: boolean) { + // no absolute paths (unless this is the root) + if (!!components[0] === !(this instanceof VirtualDirectoryRoot)) return undefined; + + // no relative paths + if (components[1] === "..") return undefined; + + // walk the components + let directory: VirtualDirectory | undefined = this; + for (let i = this instanceof VirtualDirectoryRoot ? 0 : 1; i < components.length - 1; i++) { + directory = create ? directory.getOrAddOwnDirectory(components[i]) : directory.getOwnDirectory(components[i]); + if (directory === undefined) return undefined; + } + + return directory; + } + + private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + private getOwnEntry(name: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined { + const entry = this.getOwnEntries().get(name); + return entry && isMatch(entry, options) ? options.followSymlinks ? this.fileSystem.getRealEntry(entry) : entry : undefined; + } + + private getOwnDirectory(name: string) { + return this.getOwnEntry(name, { kind: "directory" }); + } + + private getOrAddOwnDirectory(name: string) { + return this.getOwnDirectory(name) || this.addOwnDirectory(name); + } + + private addOwnDirectory(name: string, resolver?: FileSystemResolver): VirtualDirectory | undefined { + const existing = this.getOwnEntry(name); + if (existing) { + if (!resolver && existing instanceof VirtualDirectory) { + return existing; + } + return undefined; + } + + const entry = new VirtualDirectory(this, name, resolver); + this.getOwnEntries().set(entry.name, entry); + this.emit("childAdded", entry); + return entry; + } + + private addOwnFile(name: string, content?: FileSystemResolver["getContent"] | string | undefined, options: { overwrite?: boolean } = {}): VirtualFile | undefined { + const existing = this.getOwnEntry(name); + if (existing) { + if (!options.overwrite || !(existing instanceof VirtualFile)) { + return undefined; + } + + // Remove the existing entry + this.getOwnEntries().delete(name); + } + + const entry = new VirtualFile(this, name, content); + this.getOwnEntries().set(entry.name, entry); + this.emit("childAdded", entry); + return entry; + } + + private addOwnSymlink(name: string, target: VirtualFile | VirtualDirectory): VirtualSymlink | undefined { + if (this.getOwnEntry(name)) return undefined; + const entry = target instanceof VirtualFile ? new VirtualFileSymlink(this, name, target.path) : new VirtualDirectorySymlink(this, name, target.path); + this.getOwnEntries().set(entry.name, entry); + this.emit("childAdded", entry); + return entry; + } + + private removeOwnDirectory(name: string) { + const entries = this.getOwnEntries(); + return entries.get(name) instanceof VirtualDirectory ? entries.delete(name) : false; + } + + private removeOwnFile(name: string) { + const entries = this.getOwnEntries(); + return entries.get(name) instanceof VirtualFile ? entries.delete(name) : false; + } +} + +class VirtualDirectoryRoot extends VirtualDirectory { + constructor(parent: VirtualFileSystem) { + super(parent, ""); + } + + public shadow(parent: VirtualFileSystem): VirtualDirectory { + this.shadowPreamble(parent); + const shadow = new VirtualDirectoryRoot(parent); + shadow._shadowRoot = this; + return shadow; + } +} + +export class VirtualDirectorySymlink extends VirtualDirectory { + private _targetPath: string; + private _target: VirtualDirectory | undefined; + private _symLinks = new Map(); + private _symEntries: KeyedCollection | undefined; + private _onTargetParentChildRemoved: (entry: VirtualFile | VirtualDirectory) => void; + private _onTargetChildRemoved: (entry: VirtualFile | VirtualDirectory) => void; + private _onTargetChildAdded: (entry: VirtualFile | VirtualDirectory) => void; + + constructor(parent: VirtualFileSystemContainer, name: string, target: string) { + super(parent, name); + this._targetPath = target; + this._onTargetParentChildRemoved = entry => this.onTargetParentChildRemoved(entry); + this._onTargetChildAdded = entry => this.onTargetChildAdded(entry); + this._onTargetChildRemoved = entry => this.onTargetChildRemoved(entry); + } + + public get target() { + return this._targetPath; + } + + public set target(value: string) { + this.writePreamble(); + if (this._targetPath !== value) { + this._targetPath = value; + this.invalidateTarget(); + } + } + + public get isBroken(): boolean { + return this.getRealDirectory() === undefined; + } + + public getRealDirectory(): VirtualDirectory | undefined { + this.resolveTarget(); + return this._target; + } + + public addDirectory(path: string, resolver?: FileSystemResolver): VirtualDirectory | undefined { + const target = this.getRealDirectory(); + return target && target.addDirectory(path, resolver); + } + + public addFile(path: string, content?: FileSystemResolver["getContent"] | string | undefined): VirtualFile | undefined { + const target = this.getRealDirectory(); + return target && target.addFile(path, content); + } + + public removeDirectory(path: string): boolean { + const target = this.getRealDirectory(); + return target && target.removeDirectory(path) || false; + } + + public removeFile(path: string): boolean { + const target = this.getRealDirectory(); + return target && target.removeFile(path) || false; + } + + public shadow(parent: VirtualFileSystemContainer): VirtualDirectorySymlink { + this.shadowPreamble(parent); + const shadow = new VirtualDirectorySymlink(parent, this.name, this.target); + shadow._shadowRoot = this; + return shadow; + } + + public resolveTarget(): void { + if (!this._target) { + const entry = findTarget(this.fileSystem, this.target); + if (entry instanceof VirtualDirectory) { + this._target = entry; + this._target.parent.on("childRemoved", this._onTargetParentChildRemoved); + this._target.on("childAdded", this._onTargetChildAdded); + this._target.on("childRemoved", this._onTargetChildRemoved); + } + } + } + + protected getOwnEntries(): KeyedCollection { + if (!this._symEntries) { + const target = this.getRealDirectory(); + this._symEntries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive); + if (target) { + for (const entry of target.getEntries()) { + this._symEntries.set(entry.name, this.getWrappedEntry(entry)); + } + } + } + return this._symEntries; + } + + private getWrappedEntry(entry: VirtualFile | VirtualDirectory) { + let symlink = this._symLinks.get(entry); + if (entry instanceof VirtualFile) { + if (symlink instanceof VirtualFileSymlink) { + return symlink; + } + symlink = new VirtualFileSymlink(this, entry.name, entry.path); + this._symLinks.set(entry, symlink); + } + else { + if (symlink instanceof VirtualDirectorySymlink) { + return symlink; + } + symlink = new VirtualDirectorySymlink(this, entry.name, entry.path); + this._symLinks.set(entry, symlink); + } + return symlink; + } + + private onTargetParentChildRemoved(entry: VirtualFileSystemEntry) { + if (entry !== this._target) return; + this.invalidateTarget(); + } + + private onTargetChildAdded(entry: VirtualFile | VirtualDirectory) { + const wrapped = this.getWrappedEntry(entry); + this.getOwnEntries().set(entry.name, wrapped); + this.emit("childAdded", wrapped); + } + + private onTargetChildRemoved(entry: VirtualFile | VirtualDirectory) { + const wrapped = this.getWrappedEntry(entry); + this.getOwnEntries().delete(entry.name); + this._symLinks.delete(entry); + this.emit("childRemoved", wrapped); + } + + private invalidateTarget() { + if (!this._target) return; + this._target.parent.removeListener("childRemoved", this._onTargetParentChildRemoved); + this._target.removeListener("childAdded", this._onTargetChildAdded); + this._target.removeListener("childRemoved", this._onTargetChildRemoved); + this._target = undefined; + this._symLinks.clear(); + this._symEntries = undefined; + } +} + +export class VirtualFile extends VirtualFileSystemEntry { + protected _shadowRoot: VirtualFile | undefined; + + private _content: string | undefined; + private _contentWasSet: boolean; + private _resolver: FileSystemResolver["getContent"] | undefined; + + constructor(parent: VirtualDirectory, name: string, content?: FileSystemResolver["getContent"] | string | undefined) { + super(parent, name); + this._content = typeof content === "string" ? content : undefined; + this._resolver = typeof content === "function" ? content : undefined; + this._shadowRoot = undefined; + this._contentWasSet = this._content !== undefined; + } + + public get shadowRoot(): VirtualFile | undefined { + return this._shadowRoot; + } + + public getContent(): string | undefined { + if (!this._contentWasSet) { + const resolver = this._resolver; + const shadowRoot = this._shadowRoot; + if (resolver) { + this._content = resolver(this); + this._contentWasSet = true; + } + else if (shadowRoot) { + this._content = shadowRoot.getContent(); + this._contentWasSet = true; + } + } + return this._content; + } + + public setContent(value: string | undefined) { + this.writePreamble(); + this._resolver = undefined; + this._content = value; + this._contentWasSet = true; + } + + public shadow(parent: VirtualDirectory): VirtualFile { + this.shadowPreamble(parent); + const shadow = new VirtualFile(parent, this.name); + shadow._shadowRoot = this; + shadow._contentWasSet = false; + return shadow; + } + + protected makeReadOnlyCore(): void { + } +} + +export class VirtualFileSymlink extends VirtualFile { + private _target: string; + + constructor(parent: VirtualDirectory, name: string, target: string) { + super(parent, name); + this._target = target; + } + + public get target(): string { + return this._target; + } + + public set target(value: string) { + this.writePreamble(); + this._target = value; + } + + public get isBroken(): boolean { + return this.getRealFile() === undefined; + } + + public getRealFile(): VirtualFile | undefined { + const entry = findTarget(this.fileSystem, this.target); + return entry instanceof VirtualFile ? entry : undefined; + } + + public getContent(): string | undefined { + const target = this.getRealFile(); + return target && target.getContent(); + } + + public setContent(value: string | undefined) { + const target = this.getRealFile(); + if (target) target.setContent(value); + } + + public shadow(parent: VirtualDirectory) { + this.shadowPreamble(parent); + const shadow = new VirtualFileSymlink(parent, this.name, this.target); + shadow._shadowRoot = this; + return shadow; + } +} + +export type VirtualSymlink = VirtualDirectorySymlink | VirtualFileSymlink; + +function findTarget(vfs: VirtualFileSystem, target: string, set?: Set): VirtualFile | VirtualDirectory | undefined { + const entry = vfs.getEntry(target); + if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) { + if (!set) set = new Set(); + if (set.has(entry)) return undefined; + set.add(entry); + return findTarget(vfs, entry.target, set); + } + return entry; +} + +function isMatch(entry: VirtualFile | VirtualDirectory, options: { pattern?: RegExp, kind?: "file" | "directory" }) { + return (options.pattern === undefined || options.pattern.test(entry.name)) + && (options.kind !== (entry instanceof VirtualFile ? "directory" : "file")); +} \ No newline at end of file diff --git a/src/harness2/vpath.ts b/src/harness2/vpath.ts new file mode 100644 index 00000000000..4dcf9be821b --- /dev/null +++ b/src/harness2/vpath.ts @@ -0,0 +1,155 @@ +import { compareStrings } from "./utils"; + +export function normalizeSlashes(path: string): string { + return path.replace(/\s*[\\/]\s*/g, "/").trim(); +} + +const rootRegExp = /^[\\/]([\\/](.*?[\\/](.*?[\\/])?)?)?|^[a-zA-Z]:[\\/]?|^\w+:\/{2}[^\\/]*\/?/; +function getRootLength(path: string) { + const match = rootRegExp.exec(path); + return match ? match[0].length : 0; +} + +export function isAbsolute(path: string) { + return rootRegExp.test(path); +} + +const trailingSeperatorRegExp = /[\\/]$/; +export function hasTrailingSeperator(path: string) { + return trailingSeperatorRegExp.test(path); +} + +function reduce(components: string[]) { + const normalized = [components[0]]; + for (let i = 1; i < components.length; i++) { + const component = components[i]; + if (component === ".") continue; + if (component === ".." && normalized.length > 0 && normalized[normalized.length - 1] !== "..") { + normalized.pop(); + } + else { + normalized.push(component); + } + } + return normalized; +} + +export function normalize(path: string): string { + const components = reduce(parse(path)); + return components.length > 1 && hasTrailingSeperator(path) ? format(components) + "/" : format(components); +} + +export function combine(path: string, ...paths: string[]) { + path = normalizeSlashes(path); + for (let name of paths) { + name = normalizeSlashes(name); + if (name.length === 0) continue; + if (path.length === 0 || isAbsolute(name)) { + path = name; + } + else { + path = hasTrailingSeperator(path) ? path + name : path + "/" + name; + } + } + return path; +} + +export function resolve(path: string, ...paths: string[]) { + return normalize(combine(path, ...paths)); +} + +export function relative(from: string, to: string, ignoreCase: boolean) { + if (!isAbsolute(from)) throw new Error("Path not absolute"); + if (!isAbsolute(to)) throw new Error("Path not absolute"); + + const fromComponents = reduce(parse(from)); + const toComponents = reduce(parse(to)); + + let start: number; + for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { + if (compareStrings(fromComponents[start], toComponents[start], ignoreCase)) { + break; + } + } + + if (start === 0) { + return format(toComponents); + } + + const components = toComponents.slice(start); + for (; start < fromComponents.length; start++) { + components.unshift(".."); + } + + return format(["", ...components]); +} + +export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) { + if (!isAbsolute(ancestor)) throw new Error("Path not absolute"); + if (!isAbsolute(descendant)) throw new Error("Path not absolute"); + + const ancestorComponents = reduce(parse(ancestor)); + const descendantComponents = reduce(parse(descendant)); + + let start: number; + for (start = 0; start < ancestorComponents.length && start < descendantComponents.length; start++) { + if (compareStrings(ancestorComponents[start], descendantComponents[start], ignoreCase)) { + break; + } + } + + return start === ancestorComponents.length; +} + +export function parse(path: string) { + path = normalizeSlashes(path); + const rootLength = getRootLength(path); + const root = path.substring(0, rootLength); + const rest = path.substring(rootLength).split(/\/+/g); + if (rest.length && !rest[rest.length - 1]) rest.pop(); + return [root, ...rest.map(component => component.trim())]; +} + +export function format(components: string[]) { + return components.length ? components[0] + components.slice(1).join("/") : ""; +} + +export function dirname(path: string) { + path = normalizeSlashes(path); + return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf("/"))); +} + +export function basename(path: string, ext?: string): string; +export function basename(path: string, options?: { extensions?: string[], ignoreCase?: boolean }): string; +export function basename(path: string, options?: { extensions?: string[], ignoreCase?: boolean } | string) { + path = normalizeSlashes(path); + const name = path.substr(Math.max(getRootLength(path), path.lastIndexOf("/") + 1)); + const extension = typeof options === "string" ? options.startsWith(".") ? options : "." + options : + options && options.extensions ? extname(name, options) : + undefined; + return extension ? name.slice(0, name.length - extension.length) : name; +} + +const extRegExp = /\.\w+$/; +export function extname(path: string, options?: { extensions?: string[], ignoreCase?: boolean }) { + if (options && options.extensions) { + for (let extension of options.extensions) { + if (!extension.startsWith(".")) extension = "." + extension; + if (path.length > extension.length) { + const ext = path.slice(path.length - extension.length); + if (compareStrings(ext, extension, options.ignoreCase) === 0) { + return ext; + } + } + } + return ""; + } + + const match = extRegExp.exec(path); + return match ? match[0] : ""; +} + +export function chext(path: string, ext: string, options?: { extensions?: string[], ignoreCase?: boolean }) { + const pathext = extname(path, options); + return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith(".") ? ext : "." + ext) : path; +} \ No newline at end of file diff --git a/src/services/utilities.ts b/src/services/utilities.ts index c5483251c61..211a3958d47 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1346,3 +1346,57 @@ namespace ts { return getTokenAtPosition(sourceFile, declaration.members.pos - 1, /*includeJsDocComment*/ false); } } + +// Helpers for tests +/* @internal */ +namespace ts { + export interface TypesAndSymbols { + line: number; + text: string; + type: string | undefined; + symbol: string | undefined; + declarations: { fileName: string, line: number, character: number }[] | undefined; + } + + /** + * Helper used by the test harness to collect types and symbols in a file. + */ + export function getTypesAndSymbols(program: Program, fileName: string, checked: boolean, exclude: "types" | "symbols" | undefined): TypesAndSymbols[] { + const checker = checked ? program.getDiagnosticsProducingTypeChecker() : program.getTypeChecker(); + const sourceFile = program.getSourceFile(fileName); + const results: TypesAndSymbols[] = []; + visitNode(sourceFile); + return results; + + function visitNode(node: Node) { + if (node) { + if (isPartOfExpression(node) || isIdentifier(node)) { + writeTypeAndSymbol(node); + } + forEachChild(node, visitNode); + } + } + + function writeTypeAndSymbol(node: Node) { + const start = node.getStart(); + const { line } = sourceFile.getLineAndCharacterOfPosition(start); + + // Workaround to ensure we output 'C' instead of 'typeof C' for base class expressions + const type = exclude !== "types" && node.parent && isExpressionWithTypeArgumentsInClassExtendsClause(node.parent) && checker.getTypeAtLocation(node.parent) || checker.getTypeAtLocation(node); + const symbol = exclude !== "symbols" && checker.getSymbolAtLocation(node); + const declarations = symbol && symbol.declarations && symbol.declarations.map(declaration => { + const file = declaration.getSourceFile(); + const { line, character } = file.getLineAndCharacterOfPosition(declaration.pos); + return { fileName: file.fileName, line, character }; + }); + + results.push({ + line, + text: node.getText(), + type: type && checker.typeToString(type, node.parent, TypeFormatFlags.NoTruncation), + symbol: symbol && checker.symbolToString(symbol, node.parent), + declarations: declarations + }); + } + } +} \ No newline at end of file diff --git a/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.js b/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.js index 97a51d7032d..28f5a21bdcd 100644 --- a/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.js +++ b/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.js @@ -1,4 +1,41 @@ -//// [duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts] +//// [tests/cases/compiler/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.ts] //// +//// [duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts] +export interface IPoint {} + +export module Shapes { + + export class Point implements IPoint {} + +} + +//// [duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_1.ts] +//var x = new Shapes.Point(); +//interface IPoint {} + +//module Shapes { + +// export class Point implements IPoint {} + +//} //// [duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.js] +define(["require", "exports"], function (require, exports) { + "use strict"; + exports.__esModule = true; + var Shapes; + (function (Shapes) { + var Point = (function () { + function Point() { + } + return Point; + }()); + Shapes.Point = Point; + })(Shapes = exports.Shapes || (exports.Shapes = {})); +}); +//// [duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_1.js] +//var x = new Shapes.Point(); +//interface IPoint {} +//module Shapes { +// export class Point implements IPoint {} +//} diff --git a/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.symbols b/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.symbols index 81436a6e510..e9a50da68a2 100644 --- a/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.symbols +++ b/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.symbols @@ -1,3 +1,23 @@ === tests/cases/compiler/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts === +export interface IPoint {} +>IPoint : Symbol(IPoint, Decl(duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts, 0, 0)) +export module Shapes { +>Shapes : Symbol(Shapes, Decl(duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts, 0, 26)) + + export class Point implements IPoint {} +>Point : Symbol(Point, Decl(duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts, 2, 22)) +>IPoint : Symbol(IPoint, Decl(duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts, 0, 0)) + +} + +=== tests/cases/compiler/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_1.ts === +//var x = new Shapes.Point(); +No type information for this code.//interface IPoint {} +No type information for this code. +No type information for this code.//module Shapes { +No type information for this code. +No type information for this code.// export class Point implements IPoint {} +No type information for this code. +No type information for this code.//} No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.types b/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.types index 81436a6e510..1aab9ca61f0 100644 --- a/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.types +++ b/tests/baselines/reference/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding.types @@ -1,3 +1,23 @@ === tests/cases/compiler/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_0.ts === +export interface IPoint {} +>IPoint : IPoint +export module Shapes { +>Shapes : typeof Shapes + + export class Point implements IPoint {} +>Point : Point +>IPoint : IPoint + +} + +=== tests/cases/compiler/duplicateIdentifierShouldNotShortCircuitBaseTypeBinding_1.ts === +//var x = new Shapes.Point(); +No type information for this code.//interface IPoint {} +No type information for this code. +No type information for this code.//module Shapes { +No type information for this code. +No type information for this code.// export class Point implements IPoint {} +No type information for this code. +No type information for this code.//} No type information for this code. \ No newline at end of file diff --git a/tests/baselines/reference/genericArray0.js b/tests/baselines/reference/genericArray0.js index d288f18a995..0dc6519ce89 100644 --- a/tests/baselines/reference/genericArray0.js +++ b/tests/baselines/reference/genericArray0.js @@ -1,5 +1,4 @@ //// [genericArray0.ts] - var x:number[]; diff --git a/tests/baselines/reference/genericArray0.symbols b/tests/baselines/reference/genericArray0.symbols index b757cbf348c..330a83279f3 100644 --- a/tests/baselines/reference/genericArray0.symbols +++ b/tests/baselines/reference/genericArray0.symbols @@ -1,19 +1,18 @@ === tests/cases/compiler/genericArray0.ts === - var x:number[]; ->x : Symbol(x, Decl(genericArray0.ts, 1, 3)) +>x : Symbol(x, Decl(genericArray0.ts, 0, 3)) var y = x; ->y : Symbol(y, Decl(genericArray0.ts, 4, 3)) ->x : Symbol(x, Decl(genericArray0.ts, 1, 3)) +>y : Symbol(y, Decl(genericArray0.ts, 3, 3)) +>x : Symbol(x, Decl(genericArray0.ts, 0, 3)) function map() { ->map : Symbol(map, Decl(genericArray0.ts, 4, 10)) ->U : Symbol(U, Decl(genericArray0.ts, 6, 13)) +>map : Symbol(map, Decl(genericArray0.ts, 3, 10)) +>U : Symbol(U, Decl(genericArray0.ts, 5, 13)) var ys: U[] = []; ->ys : Symbol(ys, Decl(genericArray0.ts, 7, 7)) ->U : Symbol(U, Decl(genericArray0.ts, 6, 13)) +>ys : Symbol(ys, Decl(genericArray0.ts, 6, 7)) +>U : Symbol(U, Decl(genericArray0.ts, 5, 13)) } diff --git a/tests/baselines/reference/genericArray0.types b/tests/baselines/reference/genericArray0.types index 0583049c965..f9e0500eddc 100644 --- a/tests/baselines/reference/genericArray0.types +++ b/tests/baselines/reference/genericArray0.types @@ -1,5 +1,4 @@ === tests/cases/compiler/genericArray0.ts === - var x:number[]; >x : number[] diff --git a/tests/baselines/reference/moduleResolutionWithExtensions.symbols b/tests/baselines/reference/moduleResolutionWithExtensions.symbols index 5a7f9e5b62a..d01f2206165 100644 --- a/tests/baselines/reference/moduleResolutionWithExtensions.symbols +++ b/tests/baselines/reference/moduleResolutionWithExtensions.symbols @@ -27,5 +27,4 @@ import j from "./jquery"; === /src/jquery_user_1.ts === import j from "./jquery.js" >j : Symbol(j, Decl(jquery_user_1.ts, 0, 6)) ->j : Symbol(j, Decl(jquery_user_1.ts, 0, 6)) diff --git a/tests/baselines/reference/moduleResolutionWithExtensions.types b/tests/baselines/reference/moduleResolutionWithExtensions.types index 25eb5ad1fee..7b530030eb5 100644 --- a/tests/baselines/reference/moduleResolutionWithExtensions.types +++ b/tests/baselines/reference/moduleResolutionWithExtensions.types @@ -27,5 +27,4 @@ import j from "./jquery"; === /src/jquery_user_1.ts === import j from "./jquery.js" >j : number ->j : number diff --git a/tests/baselines/reference/module_augmentUninstantiatedModule2.js b/tests/baselines/reference/module_augmentUninstantiatedModule2.js index 2e7b4d917db..ae5986f28be 100644 --- a/tests/baselines/reference/module_augmentUninstantiatedModule2.js +++ b/tests/baselines/reference/module_augmentUninstantiatedModule2.js @@ -1,6 +1,42 @@ -//// [module_augmentUninstantiatedModule2.ts] -declare var ng: ng.IAngularStatic; declare module ng { export interface IModule { name: string; } export interface IAngularStatic { module: (s: string) => IModule; } } export = ng; +//// [tests/cases/compiler/module_augmentUninstantiatedModule2.ts] //// -//// [module_augmentUninstantiatedModule2.js] +//// [app.ts] +import ng = require("angular"); +import "./moduleAugmentation"; + +var x: number = ng.getNumber(); + +//// [moduleAugmentation.ts] +import * as ng from "angular" +declare module "angular" { + export interface IAngularStatic { + getNumber: () => number; + } +} + +//// [index.d.ts] +declare var ng: ng.IAngularStatic; + +declare module ng { + export interface IModule { + name: string; + } + + export interface IAngularStatic { + module: (s: string) => IModule; + } +} + +export = ng; + + + +//// [moduleAugmentation.js] "use strict"; -module.exports = ng; +exports.__esModule = true; +//// [app.js] +"use strict"; +exports.__esModule = true; +var ng = require("angular"); +require("./moduleAugmentation"); +var x = ng.getNumber(); diff --git a/tests/baselines/reference/module_augmentUninstantiatedModule2.symbols b/tests/baselines/reference/module_augmentUninstantiatedModule2.symbols index 42e27aa9eae..252f174ef0c 100644 --- a/tests/baselines/reference/module_augmentUninstantiatedModule2.symbols +++ b/tests/baselines/reference/module_augmentUninstantiatedModule2.symbols @@ -1,6 +1,55 @@ -=== tests/cases/compiler/module_augmentUninstantiatedModule2.ts === -declare var ng: ng.IAngularStatic; declare module ng { export interface IModule { name: string; } export interface IAngularStatic { module: (s: string) => IModule; } } export = ng; ->ng : Symbol(ng, Decl(module_augmentUninstantiatedModule2.ts, 0, 11), Decl(module_augmentUninstantiatedModule2.ts, 0, 34)) ->ng : Symbol(ng, Decl(module_augmentUninstantiatedModule2.ts, 0, 11), Decl(module_augmentUninstantiatedModule2.ts, 0, 34)) ->IAngularStatic : Symbol(ng.IAngularStatic, Decl(module_augmentUninstantiatedModule2.ts, 5, 4)) +=== tests/cases/compiler/app.ts === +import ng = require("angular"); +>ng : Symbol(ng, Decl(app.ts, 0, 0)) + +import "./moduleAugmentation"; + +var x: number = ng.getNumber(); +>x : Symbol(x, Decl(app.ts, 3, 3)) +>ng.getNumber : Symbol(ng.IAngularStatic.getNumber, Decl(moduleAugmentation.ts, 2, 37)) +>ng : Symbol(ng, Decl(app.ts, 0, 0)) +>getNumber : Symbol(ng.IAngularStatic.getNumber, Decl(moduleAugmentation.ts, 2, 37)) + +=== tests/cases/compiler/moduleAugmentation.ts === +import * as ng from "angular" +>ng : Symbol(ng, Decl(moduleAugmentation.ts, 0, 6)) + +declare module "angular" { + export interface IAngularStatic { +>IAngularStatic : Symbol(IAngularStatic, Decl(index.d.ts, 5, 4), Decl(moduleAugmentation.ts, 1, 26)) + + getNumber: () => number; +>getNumber : Symbol(IAngularStatic.getNumber, Decl(moduleAugmentation.ts, 2, 37)) + } +} + +=== tests/cases/compiler/node_modules/angular/index.d.ts === +declare var ng: ng.IAngularStatic; +>ng : Symbol(ng, Decl(index.d.ts, 0, 11), Decl(index.d.ts, 0, 34), Decl(moduleAugmentation.ts, 0, 29)) +>ng : Symbol(ng, Decl(index.d.ts, 0, 11), Decl(index.d.ts, 0, 34)) +>IAngularStatic : Symbol(IAngularStatic, Decl(index.d.ts, 5, 4)) + +declare module ng { +>ng : Symbol(ng, Decl(index.d.ts, 0, 11), Decl(index.d.ts, 0, 34), Decl(moduleAugmentation.ts, 0, 29)) + + export interface IModule { +>IModule : Symbol(IModule, Decl(index.d.ts, 2, 19)) + + name: string; +>name : Symbol(IModule.name, Decl(index.d.ts, 3, 29)) + } + + export interface IAngularStatic { +>IAngularStatic : Symbol(IAngularStatic, Decl(index.d.ts, 5, 4), Decl(moduleAugmentation.ts, 1, 26)) + + module: (s: string) => IModule; +>module : Symbol(IAngularStatic.module, Decl(index.d.ts, 7, 36)) +>s : Symbol(s, Decl(index.d.ts, 8, 16)) +>IModule : Symbol(IModule, Decl(index.d.ts, 2, 19)) + } +} + +export = ng; +>ng : Symbol(ng, Decl(index.d.ts, 0, 11), Decl(index.d.ts, 0, 34)) + diff --git a/tests/baselines/reference/module_augmentUninstantiatedModule2.types b/tests/baselines/reference/module_augmentUninstantiatedModule2.types index 2875ace3c1b..63d18782e7f 100644 --- a/tests/baselines/reference/module_augmentUninstantiatedModule2.types +++ b/tests/baselines/reference/module_augmentUninstantiatedModule2.types @@ -1,6 +1,56 @@ -=== tests/cases/compiler/module_augmentUninstantiatedModule2.ts === -declare var ng: ng.IAngularStatic; declare module ng { export interface IModule { name: string; } export interface IAngularStatic { module: (s: string) => IModule; } } export = ng; +=== tests/cases/compiler/app.ts === +import ng = require("angular"); >ng : ng.IAngularStatic + +import "./moduleAugmentation"; + +var x: number = ng.getNumber(); +>x : number +>ng.getNumber() : number +>ng.getNumber : () => number +>ng : ng.IAngularStatic +>getNumber : () => number + +=== tests/cases/compiler/moduleAugmentation.ts === +import * as ng from "angular" +>ng : ng.IAngularStatic + +declare module "angular" { + export interface IAngularStatic { +>IAngularStatic : IAngularStatic + + getNumber: () => number; +>getNumber : () => number + } +} + +=== tests/cases/compiler/node_modules/angular/index.d.ts === +declare var ng: ng.IAngularStatic; +>ng : IAngularStatic >ng : any ->IAngularStatic : ng.IAngularStatic +>IAngularStatic : IAngularStatic + +declare module ng { +>ng : IAngularStatic + + export interface IModule { +>IModule : IModule + + name: string; +>name : string + } + + export interface IAngularStatic { +>IAngularStatic : IAngularStatic + + module: (s: string) => IModule; +>module : (s: string) => IModule +>s : string +>IModule : IModule + } +} + +export = ng; +>ng : IAngularStatic + diff --git a/tests/baselines/reference/sourceMap-LineBreaks.js b/tests/baselines/reference/sourceMap-LineBreaks.js index 2ed7da8cf3f..36b901f084c 100644 --- a/tests/baselines/reference/sourceMap-LineBreaks.js +++ b/tests/baselines/reference/sourceMap-LineBreaks.js @@ -1,15 +1,15 @@ //// [sourceMap-LineBreaks.ts] var endsWithlineSeparator = 10; 
var endsWithParagraphSeparator = 10; 
var endsWithNextLine = 1;…var endsWithLineFeed = 1; -var endsWithCarriageReturnLineFeed = 1; +var endsWithCarriageReturnLineFeed = 1; var endsWithCarriageReturn = 1; var endsWithLineFeedCarriageReturn = 1; var endsWithLineFeedCarriageReturnLineFeed = 1; - + var stringLiteralWithLineFeed = "line 1\ -line 2"; -var stringLiteralWithCarriageReturnLineFeed = "line 1\ -line 2"; -var stringLiteralWithCarriageReturn = "line 1\ line 2"; - +line 2"; +var stringLiteralWithCarriageReturnLineFeed = "line 1\ +line 2"; +var stringLiteralWithCarriageReturn = "line 1\ line 2"; + var stringLiteralWithLineSeparator = "line 1\
line 2";
var stringLiteralWithParagraphSeparator = "line 1\
line 2";
var stringLiteralWithNextLine = "line 1\…line 2"; //// [sourceMap-LineBreaks.js] @@ -23,7 +23,7 @@ var endsWithLineFeedCarriageReturn = 1; var endsWithLineFeedCarriageReturnLineFeed = 1; var stringLiteralWithLineFeed = "line 1\ line 2"; -var stringLiteralWithCarriageReturnLineFeed = "line 1\ +var stringLiteralWithCarriageReturnLineFeed = "line 1\ line 2"; var stringLiteralWithCarriageReturn = "line 1\ line 2"; var stringLiteralWithLineSeparator = "line 1\
line 2"; diff --git a/tests/baselines/reference/sourceMap-LineBreaks.symbols b/tests/baselines/reference/sourceMap-LineBreaks.symbols index 1e527ad4d55..1f6bd2a8504 100644 --- a/tests/baselines/reference/sourceMap-LineBreaks.symbols +++ b/tests/baselines/reference/sourceMap-LineBreaks.symbols @@ -1,30 +1,47 @@ === tests/cases/compiler/sourceMap-LineBreaks.ts === -var endsWithlineSeparator = 10; 
var endsWithParagraphSeparator = 10; 
var endsWithNextLine = 1;…var endsWithLineFeed = 1; +var endsWithlineSeparator = 10; >endsWithlineSeparator : Symbol(endsWithlineSeparator, Decl(sourceMap-LineBreaks.ts, 0, 3)) -var endsWithCarriageReturnLineFeed = 1; +var endsWithParagraphSeparator = 10; >endsWithParagraphSeparator : Symbol(endsWithParagraphSeparator, Decl(sourceMap-LineBreaks.ts, 1, 3)) -var endsWithCarriageReturn = 1; var endsWithLineFeedCarriageReturn = 1; +var endsWithNextLine = 1;…var endsWithLineFeed = 1; >endsWithNextLine : Symbol(endsWithNextLine, Decl(sourceMap-LineBreaks.ts, 2, 3)) >endsWithLineFeed : Symbol(endsWithLineFeed, Decl(sourceMap-LineBreaks.ts, 2, 29)) - var endsWithLineFeedCarriageReturnLineFeed = 1; +var endsWithCarriageReturnLineFeed = 1; >endsWithCarriageReturnLineFeed : Symbol(endsWithCarriageReturnLineFeed, Decl(sourceMap-LineBreaks.ts, 3, 3)) +var endsWithCarriageReturn = 1; >endsWithCarriageReturn : Symbol(endsWithCarriageReturn, Decl(sourceMap-LineBreaks.ts, 4, 3)) -var stringLiteralWithLineFeed = "line 1\ +var endsWithLineFeedCarriageReturn = 1; >endsWithLineFeedCarriageReturn : Symbol(endsWithLineFeedCarriageReturn, Decl(sourceMap-LineBreaks.ts, 5, 3)) +var endsWithLineFeedCarriageReturnLineFeed = 1; +>endsWithLineFeedCarriageReturnLineFeed : Symbol(endsWithLineFeedCarriageReturnLineFeed, Decl(sourceMap-LineBreaks.ts, 7, 3)) + +var stringLiteralWithLineFeed = "line 1\ +>stringLiteralWithLineFeed : Symbol(stringLiteralWithLineFeed, Decl(sourceMap-LineBreaks.ts, 9, 3)) + line 2"; var stringLiteralWithCarriageReturnLineFeed = "line 1\ ->endsWithLineFeedCarriageReturnLineFeed : Symbol(endsWithLineFeedCarriageReturnLineFeed, Decl(sourceMap-LineBreaks.ts, 7, 3)) - -line 2"; -var stringLiteralWithCarriageReturn = "line 1\ line 2"; ->stringLiteralWithLineFeed : Symbol(stringLiteralWithLineFeed, Decl(sourceMap-LineBreaks.ts, 9, 3)) - -var stringLiteralWithLineSeparator = "line 1\
line 2";
var stringLiteralWithParagraphSeparator = "line 1\
line 2";
var stringLiteralWithNextLine = "line 1\…line 2"; >stringLiteralWithCarriageReturnLineFeed : Symbol(stringLiteralWithCarriageReturnLineFeed, Decl(sourceMap-LineBreaks.ts, 11, 3)) +line 2"; +var stringLiteralWithCarriageReturn = "line 1\ +>stringLiteralWithCarriageReturn : Symbol(stringLiteralWithCarriageReturn, Decl(sourceMap-LineBreaks.ts, 13, 3)) + +line 2"; + +var stringLiteralWithLineSeparator = "line 1\ +>stringLiteralWithLineSeparator : Symbol(stringLiteralWithLineSeparator, Decl(sourceMap-LineBreaks.ts, 16, 3)) + +line 2"; +var stringLiteralWithParagraphSeparator = "line 1\ +>stringLiteralWithParagraphSeparator : Symbol(stringLiteralWithParagraphSeparator, Decl(sourceMap-LineBreaks.ts, 18, 3)) + +line 2"; +var stringLiteralWithNextLine = "line 1\…line 2"; +>stringLiteralWithNextLine : Symbol(stringLiteralWithNextLine, Decl(sourceMap-LineBreaks.ts, 20, 3)) + diff --git a/tests/baselines/reference/sourceMap-LineBreaks.types b/tests/baselines/reference/sourceMap-LineBreaks.types index 41dc77a36bf..e60a65eb5b6 100644 --- a/tests/baselines/reference/sourceMap-LineBreaks.types +++ b/tests/baselines/reference/sourceMap-LineBreaks.types @@ -1,40 +1,61 @@ === tests/cases/compiler/sourceMap-LineBreaks.ts === -var endsWithlineSeparator = 10; 
var endsWithParagraphSeparator = 10; 
var endsWithNextLine = 1;…var endsWithLineFeed = 1; +var endsWithlineSeparator = 10; >endsWithlineSeparator : number >10 : 10 -var endsWithCarriageReturnLineFeed = 1; +var endsWithParagraphSeparator = 10; >endsWithParagraphSeparator : number >10 : 10 -var endsWithCarriageReturn = 1; var endsWithLineFeedCarriageReturn = 1; +var endsWithNextLine = 1;…var endsWithLineFeed = 1; >endsWithNextLine : number >1 : 1 >endsWithLineFeed : number >1 : 1 - var endsWithLineFeedCarriageReturnLineFeed = 1; +var endsWithCarriageReturnLineFeed = 1; >endsWithCarriageReturnLineFeed : number >1 : 1 +var endsWithCarriageReturn = 1; >endsWithCarriageReturn : number >1 : 1 -var stringLiteralWithLineFeed = "line 1\ +var endsWithLineFeedCarriageReturn = 1; >endsWithLineFeedCarriageReturn : number >1 : 1 -line 2"; -var stringLiteralWithCarriageReturnLineFeed = "line 1\ +var endsWithLineFeedCarriageReturnLineFeed = 1; >endsWithLineFeedCarriageReturnLineFeed : number >1 : 1 -line 2"; -var stringLiteralWithCarriageReturn = "line 1\ line 2"; +var stringLiteralWithLineFeed = "line 1\ >stringLiteralWithLineFeed : string >"line 1\line 2" : "line 1line 2" -var stringLiteralWithLineSeparator = "line 1\
line 2";
var stringLiteralWithParagraphSeparator = "line 1\
line 2";
var stringLiteralWithNextLine = "line 1\…line 2"; +line 2"; +var stringLiteralWithCarriageReturnLineFeed = "line 1\ >stringLiteralWithCarriageReturnLineFeed : string >"line 1\line 2" : "line 1line 2" +line 2"; +var stringLiteralWithCarriageReturn = "line 1\ +>stringLiteralWithCarriageReturn : string +>"line 1\line 2" : "line 1line 2" + +line 2"; + +var stringLiteralWithLineSeparator = "line 1\ +>stringLiteralWithLineSeparator : string +>"line 1\
line 2" : "line 1line 2" + +line 2"; +var stringLiteralWithParagraphSeparator = "line 1\ +>stringLiteralWithParagraphSeparator : string +>"line 1\
line 2" : "line 1line 2" + +line 2"; +var stringLiteralWithNextLine = "line 1\…line 2"; +>stringLiteralWithNextLine : string +>"line 1\…line 2" : "line 1\u0085line 2" + diff --git a/tests/cases/compiler/APISample_compile.ts b/tests/cases/compiler/APISample_compile.ts index c49d2a7b122..25979f180ee 100644 --- a/tests/cases/compiler/APISample_compile.ts +++ b/tests/cases/compiler/APISample_compile.ts @@ -2,6 +2,8 @@ // @includebuiltfile: typescript_standalone.d.ts // @noImplicitAny:true // @strictNullChecks:true +// @noTypesBaseline: true +// @noSymbolsBaseline: true /* * Note: This test is a public API sample. The sample sources can be found diff --git a/tests/cases/compiler/APISample_linter.ts b/tests/cases/compiler/APISample_linter.ts index dc5e2644b4f..ed67ec8d812 100644 --- a/tests/cases/compiler/APISample_linter.ts +++ b/tests/cases/compiler/APISample_linter.ts @@ -2,6 +2,8 @@ // @includebuiltfile: typescript_standalone.d.ts // @noImplicitAny:true // @strictNullChecks:true +// @noTypesBaseline: true +// @noSymbolsBaseline: true /* * Note: This test is a public API sample. The sample sources can be found diff --git a/tests/cases/compiler/APISample_parseConfig.ts b/tests/cases/compiler/APISample_parseConfig.ts index f59f64dc4ad..04a68f8612b 100644 --- a/tests/cases/compiler/APISample_parseConfig.ts +++ b/tests/cases/compiler/APISample_parseConfig.ts @@ -2,6 +2,8 @@ // @includebuiltfile: typescript_standalone.d.ts // @noImplicitAny:true // @strictNullChecks:true +// @noTypesBaseline: true +// @noSymbolsBaseline: true /* * Note: This test is a public API sample. The sample sources can be found diff --git a/tests/cases/compiler/APISample_transform.ts b/tests/cases/compiler/APISample_transform.ts index a76d6b15463..fc47c251187 100644 --- a/tests/cases/compiler/APISample_transform.ts +++ b/tests/cases/compiler/APISample_transform.ts @@ -2,6 +2,8 @@ // @includebuiltfile: typescript_standalone.d.ts // @noImplicitAny:true // @strictNullChecks:true +// @noTypesBaseline: true +// @noSymbolsBaseline: true /* * Note: This test is a public API sample. The sample sources can be found diff --git a/tests/cases/compiler/APISample_watcher.ts b/tests/cases/compiler/APISample_watcher.ts index 436a1401d57..62ebb7e9cb6 100644 --- a/tests/cases/compiler/APISample_watcher.ts +++ b/tests/cases/compiler/APISample_watcher.ts @@ -2,6 +2,8 @@ // @includebuiltfile: typescript_standalone.d.ts // @noImplicitAny:true // @strictNullChecks:true +// @noTypesBaseline: true +// @noSymbolsBaseline: true /* * Note: This test is a public API sample. The sample sources can be found diff --git a/tests/cases/compiler/sourceMap-LineBreaks.ts b/tests/cases/compiler/sourceMap-LineBreaks.ts index 0a765737a69..9c08f3864c4 100644 --- a/tests/cases/compiler/sourceMap-LineBreaks.ts +++ b/tests/cases/compiler/sourceMap-LineBreaks.ts @@ -1,5 +1,6 @@ // @target: ES3 // @sourcemap: true +// @preservelines: true var endsWithlineSeparator = 10; 
var endsWithParagraphSeparator = 10; 
var endsWithNextLine = 1;…var endsWithLineFeed = 1; var endsWithCarriageReturnLineFeed = 1; var endsWithCarriageReturn = 1; var endsWithLineFeedCarriageReturn = 1; diff --git a/tests/cases/conformance/decorators/class/decoratedClassExportsCommonJS1.ts b/tests/cases/conformance/decorators/class/decoratedClassExportsCommonJS1.ts index e00adf3d63b..b3b8ad2d248 100644 --- a/tests/cases/conformance/decorators/class/decoratedClassExportsCommonJS1.ts +++ b/tests/cases/conformance/decorators/class/decoratedClassExportsCommonJS1.ts @@ -2,12 +2,10 @@ // @experimentaldecorators: true // @emitDecoratorMetadata: true // @module: commonjs -// @filename: a.ts -declare function forwardRef(x: any): any; -declare var Something: any; -@Something({ v: () => Testing123 }) -export class Testing123 { - static prop0: string; - static prop1 = Testing123.prop0; +declare var Something: any; +@Something({ v: () => Testing123 }) +export class Testing123 { + static prop0: string; + static prop1 = Testing123.prop0; } \ No newline at end of file