From 41567b2261f6cd484aadf5510a9ec897dc68fb3e Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 21 Nov 2017 19:47:13 -0800 Subject: [PATCH] Migrate tscWatchMode to vfs --- .gitignore | 1 + .vscode/tasks.json | 48 +- Gulpfile.ts | 27 +- package.json | 7 +- scripts/typemock/gulpfile.js | 29 + scripts/typemock/package.json | 35 + scripts/typemock/src/arg.ts | 293 ++ scripts/typemock/src/index.ts | 6 + scripts/typemock/src/mock.ts | 394 +++ scripts/typemock/src/spy.ts | 38 + scripts/typemock/src/stub.ts | 103 + scripts/typemock/src/tests/argTests.ts | 646 ++++ scripts/typemock/src/tests/index.ts | 5 + scripts/typemock/src/tests/mockTests.ts | 262 ++ .../typemock/src/tests/sourceMapSupport.ts | 3 + scripts/typemock/src/tests/stubTests.ts | 79 + scripts/typemock/src/tests/timersTests.ts | 305 ++ scripts/typemock/src/tests/timesTests.ts | 236 ++ scripts/typemock/src/tests/utils.ts | 17 + scripts/typemock/src/timers.ts | 475 +++ scripts/typemock/src/times.ts | 120 + scripts/typemock/src/tsconfig.json | 10 + src/harness/core.ts | 125 +- src/harness/mocks.ts | 278 +- src/harness/tsconfig.json | 1 + src/harness/typemock.ts | 98 + src/harness/unittests/compileOnSave.ts | 883 +++-- src/harness/unittests/projectErrors.ts | 239 +- .../unittests/reuseProgramStructure.ts | 4 +- src/harness/unittests/tscWatchMode.ts | 3110 +++++++---------- .../unittests/tsserverProjectSystem.ts | 28 +- src/harness/unittests/typingsInstaller.ts | 582 ++- src/harness/utils.ts | 22 + src/harness/vfs.ts | 173 +- src/harness/virtualFileSystemWithWatch.ts | 113 +- 35 files changed, 5715 insertions(+), 3080 deletions(-) create mode 100644 scripts/typemock/gulpfile.js create mode 100644 scripts/typemock/package.json create mode 100644 scripts/typemock/src/arg.ts create mode 100644 scripts/typemock/src/index.ts create mode 100644 scripts/typemock/src/mock.ts create mode 100644 scripts/typemock/src/spy.ts create mode 100644 scripts/typemock/src/stub.ts create mode 100644 scripts/typemock/src/tests/argTests.ts create mode 100644 scripts/typemock/src/tests/index.ts create mode 100644 scripts/typemock/src/tests/mockTests.ts create mode 100644 scripts/typemock/src/tests/sourceMapSupport.ts create mode 100644 scripts/typemock/src/tests/stubTests.ts create mode 100644 scripts/typemock/src/tests/timersTests.ts create mode 100644 scripts/typemock/src/tests/timesTests.ts create mode 100644 scripts/typemock/src/tests/utils.ts create mode 100644 scripts/typemock/src/timers.ts create mode 100644 scripts/typemock/src/times.ts create mode 100644 scripts/typemock/src/tsconfig.json create mode 100644 src/harness/typemock.ts diff --git a/.gitignore b/.gitignore index 40c473d13dd..5087f4983d2 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ scripts/importDefinitelyTypedTests/importDefinitelyTypedTests.js scripts/generateLocalizedDiagnosticMessages.js scripts/*.js.map scripts/typings/ +scripts/typemock/dist coverage/ internal/ **/.DS_Store diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 31928f73ce2..2f6f5286775 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,30 +1,34 @@ -// Available variables which can be used inside of strings. -// ${workspaceRoot}: the root folder of the team -// ${file}: the current opened file -// ${fileBasename}: the current opened file's basename -// ${fileDirname}: the current opened file's dirname -// ${fileExtname}: the current opened file's extension -// ${cwd}: the current working directory of the spawned process { - "version": "0.1.0", - "command": "gulp", - "isShellCommand": true, - "showOutput": "silent", + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", "tasks": [ { - "taskName": "local", - "isBuildCommand": true, - "showOutput": "silent", - "problemMatcher": [ - "$tsc" - ] + "type": "shell", + "identifier": "local", + "label": "gulp: local", + "command": "gulp", + "args": ["local"], + "group": { "kind": "build", "isDefault": true }, + "problemMatcher": ["$gulp-tsc"] }, { - "taskName": "tests", - "showOutput": "silent", - "problemMatcher": [ - "$tsc" - ] + "type": "shell", + "identifier": "tsc", + "label": "gulp: tsc", + "command": "gulp", + "args": ["tsc"], + "group": "build", + "problemMatcher": ["$gulp-tsc"] + }, + { + "type": "shell", + "identifier": "tests", + "label": "gulp: tests", + "command": "gulp", + "args": ["tests"], + "group": "build", + "problemMatcher": ["$gulp-tsc"] } ] } \ No newline at end of file diff --git a/Gulpfile.ts b/Gulpfile.ts index aedde5c33d9..75043651a37 100644 --- a/Gulpfile.ts +++ b/Gulpfile.ts @@ -602,10 +602,19 @@ gulp.task("LKG", "Makes a new LKG out of the built js files", ["clean", "dontUse return runSequence("LKGInternal", "VerifyLKG"); }); +gulp.task("typemock", () => { + const typemock = tsc.createProject("scripts/typemock/src/tsconfig.json", getCompilerSettings({}, /*useBuiltCompiler*/ true)); + return typemock.src() + .pipe(sourcemaps.init()) + .pipe(newer("scripts/typemock/dist")) + .pipe(typemock()) + .pipe(sourcemaps.write(".", { includeContent: false, destPath: "scripts/typemock/dist" })) + .pipe(gulp.dest("scripts/typemock/dist")); +}); // Task to build the tests infrastructure using the built compiler const run = path.join(builtLocalDirectory, "run.js"); -gulp.task(run, /*help*/ false, [servicesFile, tsserverLibraryFile], () => { +gulp.task(run, /*help*/ false, [servicesFile, tsserverLibraryFile, "typemock"], () => { const testProject = tsc.createProject("src/harness/tsconfig.json", getCompilerSettings({}, /*useBuiltCompiler*/ true)); return testProject.src() .pipe(newer(run)) @@ -644,7 +653,7 @@ function restoreSavedNodeEnv() { process.env.NODE_ENV = savedNodeEnv; } -function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: (e?: any) => void) { +function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: (e?: any) => void, noExit?: boolean) { const lintFlag = cmdLineOptions.lint; cleanTestDirs((err) => { if (err) { console.error(err); failWithStatus(err, 1); } @@ -720,8 +729,10 @@ function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: }); function failWithStatus(err?: any, status?: number) { - if (err || status) { - process.exit(typeof status === "number" ? status : 2); + if (!noExit) { + if (err || status) { + process.exit(typeof status === "number" ? status : 2); + } } done(); } @@ -762,6 +773,10 @@ gulp.task("runtests", runConsoleTests("mocha-fivemat-progress-reporter", /*runInParallel*/ false, done); }); +gulp.task("runtests-in-watch", ["build-rules", "tests"], done => { + runConsoleTests("min", /*runInParallel*/ false, done, /*noExit*/ true); +}); + const nodeServerOutFile = "tests/webTestServer.js"; const nodeServerInFile = "tests/webTestServer.ts"; gulp.task(nodeServerOutFile, /*help*/ false, [servicesFile], () => { @@ -1110,3 +1125,7 @@ gulp.task("default", "Runs 'local'", ["local"]); gulp.task("watch", "Watches the src/ directory for changes and executes runtests-parallel.", [], () => { gulp.watch("src/**/*.*", ["runtests-parallel"]); }); + +gulp.task("watch-no-parallel", "Watches the src/ directory for changes and executes runtests.", [], () => { + gulp.watch(["src/**/*.*", "scripts/typemock/src/**/*.*"], ["runtests-in-watch"]); +}); \ No newline at end of file diff --git a/package.json b/package.json index e329db05d85..583e59a7741 100644 --- a/package.json +++ b/package.json @@ -48,12 +48,13 @@ "@types/node": "latest", "@types/q": "latest", "@types/run-sequence": "latest", + "@types/source-map-support": "^0.4.0", "@types/through2": "latest", "@types/xml2js": "^0.4.0", - "xml2js": "^0.4.19", "browser-resolve": "^1.11.2", "browserify": "latest", "chai": "latest", + "colors": "latest", "convert-source-map": "latest", "del": "latest", "gulp": "3.X", @@ -79,9 +80,9 @@ "travis-fold": "latest", "ts-node": "latest", "tslint": "latest", + "typescript": "next", "vinyl": "latest", - "colors": "latest", - "typescript": "next" + "xml2js": "^0.4.19" }, "scripts": { "pretest": "jake tests", diff --git a/scripts/typemock/gulpfile.js b/scripts/typemock/gulpfile.js new file mode 100644 index 00000000000..127ef3dc51e --- /dev/null +++ b/scripts/typemock/gulpfile.js @@ -0,0 +1,29 @@ +const gulp = require("gulp"); +const gutil = require("gulp-util"); +const sourcemaps = require("gulp-sourcemaps"); +const tsb = require("gulp-tsb"); +const mocha = require("gulp-mocha"); +const del = require("del"); + +const src = { + compile: tsb.create("src/tsconfig.json"), + src: () => gulp.src(["src/**/*.ts"]), + dest: () => gulp.dest("dist") +}; + +gulp.task("clean", () => del(["dist/**/*"])); + +gulp.task("build", () => src.src() + .pipe(sourcemaps.init()) + .pipe(src.compile()) + .pipe(sourcemaps.write(".", { includeContent: false, destPath: "dist" })) + .pipe(gulp.dest("dist"))); + +gulp.task("test", ["build"], () => gulp + .src(["dist/tests/index.js"], { read: false }) + .pipe(mocha({ reporter: "dot" }))); + + +gulp.task("watch", ["test"], () => gulp.watch(["src/**/*"], ["test"])); + +gulp.task("default", ["test"]); \ No newline at end of file diff --git a/scripts/typemock/package.json b/scripts/typemock/package.json new file mode 100644 index 00000000000..e18761f0ba7 --- /dev/null +++ b/scripts/typemock/package.json @@ -0,0 +1,35 @@ +{ + "private": true, + "name": "typemock", + "version": "0.0.0", + "description": "JavaScript Mock object framework", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "test": "gulp test" + }, + "keywords": [ + "javascript", + "mock", + "type", + "typescript" + ], + "author": "Ron Buckton (ron.buckton@microsoft.com)", + "license": "Apache-2.0", + "devDependencies": { + "@types/chai": "^4.0.4", + "@types/mocha": "^2.2.27", + "@types/node": "^8.0.20", + "@types/source-map-support": "^0.4.0", + "chai": "^4.1.2", + "del": "^2.0.2", + "gulp": "^3.9.1", + "gulp-mocha": "^4.3.1", + "gulp-sourcemaps": "^2.6.1", + "gulp-tsb": "^2.0.5", + "merge2": "^0.3.6", + "mocha": "^2.2.5", + "source-map-support": "^0.5.0", + "typescript": "^2.6.1" + } +} diff --git a/scripts/typemock/src/arg.ts b/scripts/typemock/src/arg.ts new file mode 100644 index 00000000000..7d6feb5383d --- /dev/null +++ b/scripts/typemock/src/arg.ts @@ -0,0 +1,293 @@ +/** + * Represents an argument condition used during verification. + */ +export class Arg { + private _condition: (value: any, args: ReadonlyArray, index: number) => { valid: boolean, next?: number }; + private _message: string; + + private constructor(condition: (value: any, args: ReadonlyArray, index: number) => { valid: boolean, next?: number }, message: string) { + this._condition = condition; + this._message = message; + } + + /** + * Allows any value. + */ + public static any(): T & Arg { + return new Arg(() => ({ valid: true }), `any`); + } + + /** + * Allows a value that matches the specified condition. + * @param match The condition used to match the value. + */ + public static is(match: (value: T) => boolean): T & Arg { + return new Arg(value => ({ valid: match(value) }), `is`); + } + + /** + * Allows only a null value. + */ + public static null(): T & Arg { + return new Arg(value => ({ valid: value === null }), `null`); + } + + /** + * Allows only a non-null value. + */ + public static notNull(): T & Arg { + return Arg.not(Arg.null()); + } + + /** + * Allows only an undefined value. + */ + public static undefined(): T & Arg { + return new Arg(value => ({ valid: value === undefined }), `undefined`); + } + + /** + * Allows only a non-undefined value. + */ + public static notUndefined(): T & Arg { + return Arg.not(Arg.undefined()); + } + + /** + * Allows only an undefined or null value. + */ + public static nullOrUndefined(): T & Arg { + return Arg.or(Arg.null(), Arg.undefined()); + } + + /** + * Allows only a non-undefined, non-null value. + */ + public static notNullOrUndefined(): T & Arg { + return Arg.not(Arg.nullOrUndefined()); + } + + /** + * Allows any value within the provided range. + * @param min The minimum value. + * @param max The maximum value. + */ + public static between(min: T, max: T): T & Arg { + return new Arg(value => ({ valid: min <= value && value <= max }), `between ${min} and ${max}`); + } + + /** + * Allows any value in the provided array. + */ + public static in(values: T[]): T & Arg { + return new Arg(value => ({ valid: values.indexOf(value) > -1 }), `in ${values.join(", ")}`); + } + + /** + * Allows any value not in the provided array. + */ + public static notIn(values: T[]): T & Arg { + return Arg.not(Arg.in(values)); + } + + /** + * Allows any value that matches the provided pattern. + */ + public static match(pattern: RegExp): T & Arg { + return new Arg(value => ({ valid: pattern.test(value) }), `matches ${pattern}`); + } + + public static startsWith(text: string): string & Arg { + return new Arg(value => ({ valid: String(value).startsWith(text) }), `starts with ${text}`); + } + + public static endsWith(text: string): string & Arg { + return new Arg(value => ({ valid: String(value).endsWith(text) }), `ends with ${text}`); + } + + public static includes(text: string): string & Arg { + return new Arg(value => ({ valid: String(value).includes(text) }), `contains ${text}`); + } + + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: "string"): string & Arg; + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: "number"): number & Arg; + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: "boolean"): boolean & Arg; + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: "symbol"): symbol & Arg; + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: "object"): object & Arg; + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: "function"): ((...args: any[]) => any) & Arg; + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: "undefined"): undefined & Arg; + /** + * Allows any value with the provided `typeof` tag. + */ + public static typeof(tag: string): T & Arg; + public static typeof(tag: string): any { + return new Arg(value => ({ valid: typeof value === tag }), `typeof ${tag}`); + } + + public static string() { return this.typeof("string"); } + public static number() { return this.typeof("number"); } + public static boolean() { return this.typeof("boolean"); } + public static symbol() { return this.typeof("symbol"); } + public static object() { return this.typeof("object"); } + public static function() { return this.typeof("function"); } + + /** + * Allows any value that is an instance of the provided function. + * @param type The expected constructor. + */ + public static instanceof(type: TClass): TClass["prototype"] & Arg { + return new Arg(value => ({ valid: value instanceof type }), `instanceof ${type.name}`); + } + + /** + * Allows any value that has the provided property names in its prototype chain. + */ + public static has(...names: string[]): T & Arg { + return new Arg(value => ({ valid: names.filter(name => name in value).length === names.length }), `has ${names.join(", ")}`); + } + + /** + * Allows any value that has the provided property names on itself but not its prototype chain. + */ + public static hasOwn(...names: string[]): T & Arg { + return new Arg(value => ({ valid: names.filter(name => Object.prototype.hasOwnProperty.call(value, name)).length === names.length }), `hasOwn ${names.join(", ")}`); + } + + /** + * Allows any value that matches the provided condition for the rest of the arguments in the call. + * @param condition The optional condition for each other element. + */ + public static rest(condition?: T | (T & Arg)): T & Arg { + if (condition === undefined) { + return new Arg((_, args) => ({ valid: true, next: args.length }), `rest`); + } + + const arg = Arg.from(condition); + return new Arg( + (_, args, index) => { + while (index < args.length) { + const { valid, next } = Arg.validate(arg, args, index); + if (!valid) return { valid: false }; + index = typeof next === "undefined" ? index + 1 : next; + } + return { valid: true, next: index }; + }, + `rest ${arg._message}` + ); + } + + /** + * Negates a condition. + */ + public static not(value: T | (T & Arg)): T & Arg { + const arg = Arg.from(value); + return new Arg((value, args, index) => { + const result = arg._condition(value, args, index); + return { valid: !result.valid, next: result.next }; + }, `not ${arg._message}`); + } + + /** + * Combines conditions, where all conditions must be `true`. + */ + public static and(...args: ((T & Arg) | T)[]): T & Arg { + const conditions = args.map(Arg.from); + return new Arg((value, args, index) => { + for (const condition of conditions) { + const result = condition._condition(value, args, index); + if (!result.valid) return { valid: false }; + } + return { valid: true }; + }, conditions.map(condition => condition._message).join(" and ")); + } + + /** + * Combines conditions, where no condition may be `true`. + */ + public static nand(...args: ((T & Arg) | T)[]): T & Arg { + return this.not(this.and(...args)); + } + + /** + * Combines conditions, where any conditions may be `true`. + */ + public static or(...args: ((T & Arg) | T)[]): T & Arg { + const conditions = args.map(Arg.from); + return new Arg((value, args, index) => { + for (const condition of conditions) { + const result = condition._condition(value, args, index); + if (result.valid) return { valid: true }; + } + return { valid: false }; + }, conditions.map(condition => condition._message).join(" or ")); + } + + /** + * Combines conditions, where all conditions must be `true`. + */ + public static nor(...args: ((T & Arg) | T)[]): T & Arg { + return this.not(this.or(...args)); + } + + /** + * Ensures the value is a `Condition` + * @param value The value to coerce + * @returns The condition + */ + public static from(value: T): T & Arg { + if (value instanceof Arg) { + return value; + } + + return new Arg(v => ({ valid: is(v, value) }), JSON.stringify(value)); + } + + /** + * Validates the arguments against the condition. + * @param args The arguments for the execution + * @param index The current index into the `args` array + * @returns An object that specifies whether the condition is `valid` and what the `next` index should be. + */ + public static validate(arg: Arg, args: ReadonlyArray, index: number): { valid: boolean, next?: number } { + const value = index >= 0 && index < args.length ? args[index] : undefined; + const { valid, next } = arg._condition(value, args, index); + return valid + ? { valid: true, next: next === undefined ? index + 1 : next } + : { valid: false }; + } + + /** + * Gets a string that represents this condition. + */ + public toString(): string { + return `<${this._message}>`; + } +} + +/** + * SameValueZero (from ECMAScript spec), which has stricter equality sematics than "==" or "===". + */ +function is(x: any, y: any) { + return (x === y) ? (x !== 0 || 1 / x === 1 / y) : (x !== x && y !== y); +} \ No newline at end of file diff --git a/scripts/typemock/src/index.ts b/scripts/typemock/src/index.ts new file mode 100644 index 00000000000..aa94febd01b --- /dev/null +++ b/scripts/typemock/src/index.ts @@ -0,0 +1,6 @@ +export { Arg } from "./arg"; +export { Times } from "./times"; +export { Mock, Returns, Throws } from "./mock"; +export { Spy, Callable, Constructable } from "./spy"; +export { Stub } from "./stub"; +export { Timers, Timer, Timeout, Interval, Immediate, AnimationFrame } from "./timers"; \ No newline at end of file diff --git a/scripts/typemock/src/mock.ts b/scripts/typemock/src/mock.ts new file mode 100644 index 00000000000..85d441c8382 --- /dev/null +++ b/scripts/typemock/src/mock.ts @@ -0,0 +1,394 @@ +import { Times } from "./times"; +import { Arg } from "./arg"; + +const weakHandler = new WeakMap>(); + +function noop() {} + +function getHandler(value: object) { + return weakHandler.get(value); +} + +export interface Returns { + returns: U; +} + +export interface Throws { + throws: any; +} + +/** + * A mock version of another oject + */ +export class Mock { + private _target: T; + private _handler = new MockHandler(); + private _proxy: T; + private _revoke: () => void; + + /** + * A mock version of another object + * @param target The object to mock. + * @param setups Optional setups to use + */ + constructor(target: T = {}, setups?: Partial) { + this._target = target; + + const { proxy, revoke } = Proxy.revocable(this._target, this._handler); + this._proxy = proxy; + this._revoke = revoke; + + weakHandler.set(proxy, this._handler); + + if (setups) { + this.setup(setups); + } + } + + /** + * Gets the mock version of the target + */ + public get value(): T { + return this._proxy; + } + + /** + * Performs setup of the mock object, overriding the target object's functionality with that provided by the setup + * @param callback A function used to set up a method result. + * @param result An object used to describe the result of the method. + * @returns This mock instance. + */ + public setup(callback: (value: T) => U, result?: Returns | Throws): Mock; + /** + * Performs setup of the mock object, overriding the target object's functionality with that provided by the setup + * @param setups An object whose members are used instead of the target object. + * @returns This mock instance. + */ + public setup(setups: Partial): Mock; + public setup(setup: Partial | ((value: T) => U), result?: Returns | Throws): Mock { + if (typeof setup === "function") { + this._handler.setupCall(setup, result); + } + else { + this._handler.setupMembers(setup); + } + return this; + } + + /** + * Performs verification that a specific action occurred. + * @param callback A callback that simulates the expected action. + * @param times The number of times the action should have occurred. + * @returns This mock instance. + */ + public verify(callback: (value: T) => any, times: Times): Mock { + this._handler.verify(callback, times); + return this; + } + + public revoke() { + this._handler.revoke(); + this._revoke(); + } +} + +class Setup { + public recording: Recording; + public result: Partial & Throws> | undefined; + + constructor (recording: Recording, result?: Returns | Throws) { + this.recording = recording; + this.result = result; + } + + public static evaluate(setups: ReadonlyArray | undefined, trap: string, args: any[], newTarget?: any) { + if (setups) { + for (let i = setups.length - 1; i >= 0; i--) { + const setup = setups[i]; + if (setup.recording.trap === trap && + setup.recording.newTarget === newTarget && + setup.matchArguments(args)) { + return setup.getResult(); + } + } + } + throw new Error("No matching setups."); + } + + public matchArguments(args: any[]) { + return this.recording.matchArguments(args); + } + + public getResult() { + if (this.result) { + if (this.result.throws) { + throw this.result.throws; + } + return this.result.returns; + } + return undefined; + } +} + +class Recording { + public readonly trap: string; + public readonly name: PropertyKey | undefined; + public readonly args: ReadonlyArray; + public readonly newTarget: any; + + private _conditions: ReadonlyArray | undefined; + + constructor(trap: string, name: PropertyKey | undefined, args: ReadonlyArray, newTarget?: any) { + this.trap = trap; + this.name = name; + this.args = args || []; + this.newTarget = newTarget; + } + + public get conditions() { + return this._conditions || (this._conditions = this.args.map(Arg.from)); + } + + public toString(): string { + return `${this.trap} ${this.name || ""}(${this.conditions.join(", ")})${this.newTarget ? ` [${this.newTarget.name}]` : ``}`; + } + + public matchRecording(recording: Recording) { + if (recording.trap !== this.trap || + recording.name !== this.name || + recording.newTarget !== this.newTarget) { + return false; + } + + return this.matchArguments(recording.args); + } + + public matchArguments(args: ReadonlyArray) { + let argi = 0; + while (argi < this.conditions.length) { + const condition = this.conditions[argi]; + const { valid, next } = Arg.validate(condition, args, argi); + if (!valid) { + return false; + } + argi = typeof next === "number" ? next : argi + 1; + } + if (argi < args.length) { + return false; + } + return true; + } +} + +class MockHandler implements ProxyHandler { + private readonly overrides = Object.create(null); + private readonly recordings: Recording[] = []; + private readonly selfSetups: Setup[] = []; + private readonly memberSetups = new Map(); + private readonly methodTargets = new WeakMap(); + private readonly methodProxies = new Map(); + private readonly methodRevocations = new Set<() => void>(); + + constructor() { + } + + public apply(target: T | Function, thisArg: any, argArray: any[]): any { + if (typeof target === "function") { + this.recordings.push(new Recording("apply", undefined, argArray)); + return this.selfSetups.length > 0 + ? Setup.evaluate(this.selfSetups, "apply", argArray) + : Reflect.apply(target, thisArg, argArray); + } + return undefined; + } + + public construct(target: T | Function, argArray: any[], newTarget?: any): any { + if (typeof target === "function") { + this.recordings.push(new Recording("construct", undefined, argArray, newTarget)); + return this.selfSetups.length > 0 + ? Setup.evaluate(this.selfSetups, "construct", argArray, newTarget) + : Reflect.construct(target, argArray, newTarget); + } + return undefined; + } + + public get(target: T, name: PropertyKey, receiver: any): any { + this.recordings.push(new Recording("get", name, [])); + const value = Reflect.get(this.getTarget(target, name), name, receiver); + return typeof value === "function" ? this.getMethod(name, value) : value; + } + + public set(target: T, name: PropertyKey, value: any, receiver: any): boolean { + this.recordings.push(new Recording("set", name, [value])); + if (typeof value === "function" && this.methodTargets.has(value)) { + value = this.methodTargets.get(value); + } + + return Reflect.set(this.getTarget(target, name), name, value, receiver); + } + + public invoke(proxy: T, name: PropertyKey, method: Function, argArray: any[]): any { + this.recordings.push(new Recording("invoke", name, argArray)); + return Reflect.apply(method, proxy, argArray); + } + + public setupCall(callback: (value: any) => any, result: Returns | Throws | undefined) { + const recording = capture(callback); + if (recording.name === undefined) { + this.selfSetups.push(new Setup(recording, result)); + } + else { + let setups = this.memberSetups.get(recording.name); + if (!setups) { + this.memberSetups.set(recording.name, setups = []); + if (recording.trap === "invoke") { + this.defineMethod(recording.name); + } + else { + this.defineAccessor(recording.name); + } + } + else { + if ((setups[0].recording.trap === "invoke") !== (recording.trap === "invoke")) { + throw new Error(`Cannot mix method and acessor setups for the same property.`); + } + } + + setups.push(new Setup(recording, result)); + } + } + + public setupMembers(setup: object) { + for (const propertyKey of Reflect.ownKeys(setup)) { + const descriptor = Reflect.getOwnPropertyDescriptor(setup, propertyKey); + if (descriptor) { + if (propertyKey in this.overrides) { + throw new Error(`Property '${propertyKey.toString()}' already exists.`); + } + Reflect.defineProperty(this.overrides, propertyKey, descriptor); + } + } + } + + public verify(callback: (value: T) => any, times: Times): void { + const expectation = capture(callback); + + let count: number = 0; + for (const recording of this.recordings) { + if (expectation.matchRecording(recording)) { + count++; + } + } + + times.check(count, `An error occured when verifying expectation: ${expectation}`); + } + + public getTarget(target: T, name: PropertyKey) { + return name in this.overrides ? this.overrides : target; + } + + public getMethod(name: PropertyKey, value: Function): Function { + const proxy = this.methodProxies.get(name); + if (proxy && this.methodTargets.get(proxy) === value) { + return proxy; + } + else { + const { proxy, revoke } = Proxy.revocable(value, new MethodHandler(name)); + this.methodProxies.set(name, proxy); + this.methodRevocations.add(revoke); + this.methodTargets.set(proxy, value); + return proxy; + } + } + + public revoke() { + for (const revoke of this.methodRevocations) { + revoke(); + } + } + + private defineMethod(name: PropertyKey) { + const setups = this.memberSetups; + this.setupMembers({ + [name](...args: any[]) { + return Setup.evaluate(setups.get(name), "invoke", args); + } + }); + } + + private defineAccessor(name: PropertyKey) { + const setups = this.memberSetups; + this.setupMembers({ + get [name]() { + return Setup.evaluate(setups.get(name), "get", []); + }, + set [name](value: any) { + Setup.evaluate(setups.get(name), "set", [value]); + } + }); + } +} + +class MethodHandler { + public name: PropertyKey; + + constructor(name: PropertyKey) { + this.name = name; + } + + public apply(target: Function, thisArgument: any, argumentsList: any[]): any { + const handler = getHandler(thisArgument); + return handler + ? handler.invoke(thisArgument, this.name, target, argumentsList) + : Reflect.apply(target, thisArgument, argumentsList); + } +} + +class CapturingHandler { + public recording: Recording | undefined; + + private _name: PropertyKey; + private _method: Function; + + constructor() { + this._method = (...args: any[]) => { + this.recording = new Recording("invoke", this._name, args); + }; + } + + public apply(_target: object, _thisArg: any, argArray: any[]): any { + this.recording = new Recording("apply", /*name*/ undefined, argArray); + return undefined; + } + + public construct(_target: object, argArray: any[], newTarget?: any): any { + this.recording = new Recording("construct", /*name*/ undefined, argArray, newTarget); + return undefined; + } + + public get(_target: object, name: PropertyKey, _receiver: any): any { + this.recording = new Recording("get", name, []); + this._name = name; + return this._method; + } + + public set(_target: object, name: PropertyKey, value: any, _receiver: any): boolean { + this.recording = new Recording("set", name, [value]); + return true; + } +} + +function capture(callback: (value: T) => U): Recording { + const handler = new CapturingHandler(); + const { proxy, revoke } = Proxy.revocable(noop, handler); + try { + callback(proxy); + if (!handler.recording) { + throw new Error("Nothing was captured."); + } + return handler.recording; + } + finally { + revoke(); + } +} \ No newline at end of file diff --git a/scripts/typemock/src/spy.ts b/scripts/typemock/src/spy.ts new file mode 100644 index 00000000000..2bb21e6e21d --- /dev/null +++ b/scripts/typemock/src/spy.ts @@ -0,0 +1,38 @@ +import { Mock } from "./mock"; +import { Times } from "./times"; +import { Arg } from "./arg"; + +function noop() {} + +export type Callable = ((...args: any[]) => any); + +export type Constructable = (new (...args: any[]) => any); + +export class Spy { + private _mock: Mock; + + constructor(target = noop) { + this._mock = new Mock(target); + } + + public get value(): T { + return this._mock.value; + } + + public verify(callback: (value: T) => any, times: Times): this { + this._mock.verify(callback, times); + return this; + } + + public called(times: Times): this { + return this.verify(_ => (_)(Arg.rest()), times); + } + + public constructed(times: Times): this { + return this.verify(_ => new (_)(Arg.rest()), times); + } + + public revoke(): void { + this._mock.revoke(); + } +} diff --git a/scripts/typemock/src/stub.ts b/scripts/typemock/src/stub.ts new file mode 100644 index 00000000000..e643aae44d6 --- /dev/null +++ b/scripts/typemock/src/stub.ts @@ -0,0 +1,103 @@ +/** + * Temporarily injects a value into an object property + */ +export class Stub { + private _target: T; + private _key: K; + private _value: any; + private _originalValue: any; + private _installed: boolean = false; + + /** + * Temporarily injects a value into an object property + * @param target The target object into which to inject a property + * @param propertyKey The name of the property to inject + * @param value The value to inject + */ + constructor(target: T, propertyKey: K, value?: T[K]) { + this._target = target; + this._key = propertyKey; + this._value = arguments.length === 2 ? target[propertyKey] : value; + } + + public get target() { + return this._target; + } + + public get key() { + return this._key; + } + + public get stubValue(): T[K] { + return this._installed ? this.currentValue : this._value; + } + + public set stubValue(value: T[K]) { + if (this._installed) { + this._target[this._key] = value; + } + this._value = value; + } + + public get originalValue(): T[K] { + if (this._installed) { + return this._originalValue; + } + else { + return this.currentValue; + } + } + + public get currentValue(): T[K] { + return this._target[this._key]; + } + + /** + * Gets a value indicating whether the Stub is currently installed. + */ + public get installed(): boolean { + return this._installed; + } + + /** + * Installs the stub + */ + public install(): void { + if (this._installed) return; + this._originalValue = this._target[this._key]; + this._target[this._key] = this._value; + this._installed = true; + } + + /** + * Uninstalls the stub + */ + public uninstall(): void { + if (!this._installed) return; + this._target[this._key] = this._originalValue; + this._installed = false; + this._originalValue = null; + } + + public static exec(target: T, propertyKey: K, value: T[K], action: () => V) { + const stub = new Stub(target, propertyKey, value); + return stub.exec(action); + } + + /** + * Executes `action` with the stub installed. + */ + public exec(action: () => V): V { + if (this._installed) { + return action(); + } + try { + this.install(); + return action(); + } + finally { + this.uninstall(); + } + } +} + diff --git a/scripts/typemock/src/tests/argTests.ts b/scripts/typemock/src/tests/argTests.ts new file mode 100644 index 00000000000..1203097ec5c --- /dev/null +++ b/scripts/typemock/src/tests/argTests.ts @@ -0,0 +1,646 @@ +import "./sourceMapSupport"; +import { Arg } from "../arg"; +import { assert } from "chai"; + +describe("arg", () => { + describe("any", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.any()); + + // act + const result = Arg.validate(target, ["a"], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.any()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("is", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.is(value => value === "a")); + + // act + const result = Arg.validate(target, ["a"], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.is(value => value === "a")); + + // act + const result = Arg.validate(target, ["b"], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.is(value => value === "a")); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("notNull", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.notNull()); + + // act + const result = Arg.validate(target, [{}], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.notNull()); + + // act + const result = Arg.validate(target, [null], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.notNull()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("null", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.null()); + + // act + const result = Arg.validate(target, [null], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.null()); + + // act + const result = Arg.validate(target, [{}], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.null()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("notUndefined", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.notUndefined()); + + // act + const result = Arg.validate(target, [{}], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.notUndefined()); + + // act + const result = Arg.validate(target, [undefined], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.notUndefined()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("undefined", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.undefined()); + + // act + const result = Arg.validate(target, [undefined], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.undefined()); + + // act + const result = Arg.validate(target, [{}], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.undefined()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("notNullOrUndefined", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.notNullOrUndefined()); + + // act + const result = Arg.validate(target, [{}], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("invalid (null)", () => { + // arrange + const target = Arg.from(Arg.notNullOrUndefined()); + + // act + const result = Arg.validate(target, [null], 0); + + // assert + assert.isFalse(result.valid); + }); + it("invalid (undefined)", () => { + // arrange + const target = Arg.from(Arg.notNullOrUndefined()); + + // act + const result = Arg.validate(target, [undefined], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.notNullOrUndefined()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("nullOrUndefined", () => { + it("valid (null)", () => { + // arrange + const target = Arg.from(Arg.nullOrUndefined()); + + // act + const result = Arg.validate(target, [null], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("valid (undefined)", () => { + // arrange + const target = Arg.from(Arg.nullOrUndefined()); + + // act + const result = Arg.validate(target, [undefined], 0); + + // assert + assert.isTrue(result.valid); + assert.strictEqual(result.next, 1); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.nullOrUndefined()); + + // act + const result = Arg.validate(target, [{}], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.nullOrUndefined()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("between", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.between(1, 3)); + + // act + const min = Arg.validate(target, [1], 0); + const mid = Arg.validate(target, [2], 0); + const max = Arg.validate(target, [3], 0); + + // assert + assert.isTrue(min.valid); + assert.isTrue(mid.valid); + assert.isTrue(max.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.between(1, 3)); + + // act + const before = Arg.validate(target, [0], 0); + const after = Arg.validate(target, [4], 0); + + // assert + assert.isFalse(before.valid); + assert.isFalse(after.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.between(1, 3)); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("in", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.in(["a", "b"])); + + // act + const result = Arg.validate(target, ["a"], 0); + + // assert + assert.isTrue(result.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.in(["a", "b"])); + + // act + const result = Arg.validate(target, ["c"], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.in(["a", "b"])); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("notIn", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.notIn(["a", "b"])); + + // act + const result = Arg.validate(target, ["c"], 0); + + // assert + assert.isTrue(result.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.notIn(["a", "b"])); + + // act + const result = Arg.validate(target, ["a"], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.notIn(["a", "b"])); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("match", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.match(/^a$/)); + + // act + const result = Arg.validate(target, ["a"], 0); + + // assert + assert.isTrue(result.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.match(/^a$/)); + + // act + const result = Arg.validate(target, ["b"], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.match(/^a$/)); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("typeof", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.typeof("number")); + + // act + const result = Arg.validate(target, [1], 0); + + // assert + assert.isTrue(result.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.typeof("number")); + + // act + const result = Arg.validate(target, ["a"], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.typeof("number")); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("instanceof", () => { + it("valid", () => { + // arrange + class C {} + const target = Arg.from(Arg.instanceof(C)); + + // act + const result = Arg.validate(target, [new C()], 0); + + // assert + assert.isTrue(result.valid); + }); + it("invalid", () => { + // arrange + class C {} + const target = Arg.from(Arg.instanceof(C)); + + // act + const result = Arg.validate(target, [{}], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + class C {} + const target = Arg.from(Arg.instanceof(C)); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("has", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.has("a")); + + // act + const own = Arg.validate(target, [{ a: 1 }], 0); + const proto = Arg.validate(target, [{ __proto__: { a: 1 } }], 0); + + // assert + assert.isTrue(own.valid); + assert.isTrue(proto.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.has("a")); + + // act + const result = Arg.validate(target, [{ b: 1 }], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.has("a")); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("hasOwn", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.hasOwn("a")); + + // act + const own = Arg.validate(target, [{ a: 1 }], 0); + + // assert + assert.isTrue(own.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.hasOwn("a")); + + // act + const result = Arg.validate(target, [{ b: 1 }], 0); + const proto = Arg.validate(target, [{ __proto__: { a: 1 } }], 0); + + // assert + assert.isFalse(result.valid); + assert.isFalse(proto.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.hasOwn("a")); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("rest", () => { + describe("no condition", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.rest()); + + // act + const empty = Arg.validate(target, [], 0); + const multiple = Arg.validate(target, ["a", "b"], 0); + + // assert + assert.isTrue(empty.valid); + assert.strictEqual(empty.next, 0); + assert.isTrue(multiple.valid); + assert.strictEqual(multiple.next, 2); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.rest()); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + describe("condition", () => { + it("valid", () => { + // arrange + const target = Arg.from(Arg.rest(Arg.typeof("string"))); + + // act + const empty = Arg.validate(target, [], 0); + const multiple = Arg.validate(target, ["a", "b"], 0); + + // assert + assert.isTrue(empty.valid); + assert.strictEqual(empty.next, 0); + assert.isTrue(multiple.valid); + assert.strictEqual(multiple.next, 2); + }); + it("invalid", () => { + // arrange + const target = Arg.from(Arg.rest(Arg.typeof("string"))); + + // act + const result = Arg.validate(target, ["a", 1], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from(Arg.rest(Arg.typeof("string"))); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, ``); + }); + }); + }); + describe("from", () => { + it("valid", () => { + // arrange + const target = Arg.from("a"); + + // act + const result = Arg.validate(target, ["a"], 0); + + // assert + assert.isTrue(result.valid); + }); + it("invalid", () => { + // arrange + const target = Arg.from("a"); + + // act + const result = Arg.validate(target, ["b"], 0); + + // assert + assert.isFalse(result.valid); + }); + it("toString", () => { + // arrange + const target = Arg.from("a"); + + // act + const result = target.toString(); + + // assert + assert.strictEqual(result, `<"a">`); + }); + }); +}); \ No newline at end of file diff --git a/scripts/typemock/src/tests/index.ts b/scripts/typemock/src/tests/index.ts new file mode 100644 index 00000000000..45a2246e286 --- /dev/null +++ b/scripts/typemock/src/tests/index.ts @@ -0,0 +1,5 @@ +import "./argTests"; +import "./timesTests"; +import "./mockTests"; +import "./stubTests"; +import "./timersTests"; \ No newline at end of file diff --git a/scripts/typemock/src/tests/mockTests.ts b/scripts/typemock/src/tests/mockTests.ts new file mode 100644 index 00000000000..38efbdb04c7 --- /dev/null +++ b/scripts/typemock/src/tests/mockTests.ts @@ -0,0 +1,262 @@ +import "./sourceMapSupport"; +import { Mock } from "../mock"; +import { Stub } from "../stub"; +import { Arg } from "../arg"; +import { Times } from "../times"; +import { recordError } from "./utils"; +import { assert } from "chai"; + +describe("mock", () => { + it("mock get with no setups", () => { + // arrange + const target = { a: 1 }; + const mock = new Mock(target); + + // act + const result = mock.value.a; + + // assert + assert.equal(1, result); + }); + it("mock setup property get with return", () => { + // arrange + const target = { a: 1 }; + const mock = new Mock(target, { get a() { return 2; } }); + + // act + const result = mock.value.a; + + // assert + assert.equal(2, result); + }); + it("mock setup property get with throw", () => { + // arrange + const target = { a: 1 }; + const error = new Error("error"); + const mock = new Mock(target, { get a(): number { throw error; } }); + + // act + const e = recordError(() => mock.value.a); + + // assert + assert.strictEqual(error, e); + }); + it("mock setup property set", () => { + // arrange + let _a: number | undefined; + const target = { a: 1 }; + const mock = new Mock(target, { set a(value: number) { _a = value; } }); + + // act + mock.value.a = 2; + + // assert + assert.equal(2, _a); + assert.equal(1, target.a); + }); + it("mock setup property set with throw", () => { + // arrange + const target = { a: 1 }; + const error = new Error("error"); + const mock = new Mock(target, { set a(value: number) { throw error; } }); + + // act + const e = recordError(() => mock.value.a = 2); + + // assert + assert.strictEqual(error, e); + }); + it("mock setup method call no setups", () => { + // arrange + const target = { a() { return 1; } }; + const mock = new Mock(target); + + // act + const result = mock.value.a(); + + // assert + assert.equal(1, result); + }); + it("mock setup method callback", () => { + // arrange + const target = { a() { return 1; } }; + const mock = new Mock(target, { a() { return 2; } }); + + // act + const result = mock.value.a(); + + // assert + assert.equal(2, result); + }); + it("mock setup method callback throws", () => { + // arrange + const target = { a() { return 1; } }; + const error = new Error("error"); + const mock = new Mock(target, { a(): number { throw error; } }); + + // act + const e = recordError(() => mock.value.a()); + + // assert + assert.strictEqual(error, e); + }); + it("mock setup new property", () => { + // arrange + const target = { a: 1 }; + const mock = new Mock(target, { b: 2 }); + + // act + const result = (mock.value).b; + + // assert + assert.equal(2, result); + }); + it("mock setup new method", () => { + // arrange + const target = { a: 1 }; + const mock = new Mock(target, { b() { return 2; } }); + + // act + const result = (mock.value).b(); + + // assert + assert.equal(2, result); + }); + it("mock verify get no setups, not called throws", () => { + // arrange + const target = { a: 1 }; + const mock = new Mock(target); + + // act + const e = recordError(() => mock.verify(_ => _.a, Times.once())); + + // assert + assert.instanceOf(e, Error); + }); + it("mock verify get no setups, called passes", () => { + // arrange + const target = { a: 1 }; + const mock = new Mock(target); + const result = mock.value.a; + + // act + const e = recordError(() => mock.verify(_ => _.a, Times.once())); + + // assert + assert.isUndefined(e); + }); + it("mock verify setup get, called passes", () => { + // arrange + const target = { a: 1 }; + const mock = new Mock(target, { get a() { return 2 } }); + const result = mock.value.a; + + // act + const e = recordError(() => mock.verify(_ => _.a, Times.once())); + + // assert + assert.isUndefined(e); + }); + it("mock verify method no setups, not called throws", () => { + // arrange + const target = { a() { return 1; } }; + const mock = new Mock(target); + + // act + const e = recordError(() => mock.verify(_ => _.a(), Times.once())); + + // assert + assert.instanceOf(e, Error); + }); + it("mock verify method no setups, called passes", () => { + // arrange + const target = { a() { return 1; } }; + const mock = new Mock(target); + const result = mock.value.a(); + + // act + const e = recordError(() => mock.verify(_ => _.a(), Times.once())); + + // assert + assert.isUndefined(e); + }); + it("mock verify setup method, called passes", () => { + // arrange + const target = { a(x: number) { return x + 1; } }; + const mock = new Mock(target, { + a(x: number) { + return x + 2; + } + }); + const result = mock.value.a(3); + + // act + const e = recordError(() => mock.verify(_ => _.a(Arg.number()), Times.once())); + + // assert + assert.isUndefined(e); + }); + it("mock setup method using callback", () => { + // arrange + const mock = new Mock<{ a(x: number): number; }>(); + mock.setup(_ => _.a(1), { returns: 2 }); + + // act + const result = mock.value.a(1); + + // assert + assert.strictEqual(result, 2); + }); + it("mock setup setter/getter using callback", () => { + // arrange + const mock = new Mock<{ a: number }>(); + mock.setup(_ => _.a, { returns: 2 }); + mock.setup(_ => _.a = Arg.any()); + + // act + const result = mock.value.a; + mock.value.a = 3; + + // assert + assert.strictEqual(result, 2); + }); + it("mock setup getter only using callback", () => { + // arrange + const mock = new Mock<{ a: number }>(); + mock.setup(_ => _.a, { returns: 2 }); + + // act + const result = mock.value.a; + const err = recordError(() => mock.value.a = 3); + + // assert + assert.strictEqual(result, 2); + assert.instanceOf(err, Error); + }); + it("mock setup setter only using callback", () => { + // arrange + const mock = new Mock<{ a: number }>(); + mock.setup(_ => _.a = 2); + + // act + const err1 = recordError(() => mock.value.a); + const err2 = recordError(() => mock.value.a = 2); + const err3 = recordError(() => mock.value.a = 3); + + // assert + assert.instanceOf(err1, Error); + assert.isUndefined(err2); + assert.instanceOf(err3, Error); + }); + it("mock setup function only using callback", () => { + // arrange + const mock = new Mock<(x: number) => number>(x => 0); + mock.setup(_ => _(Arg.number()), { returns: 2 }); + + // act + const result = mock.value(1); + + // assert + assert.strictEqual(result, 2); + }); +}); \ No newline at end of file diff --git a/scripts/typemock/src/tests/sourceMapSupport.ts b/scripts/typemock/src/tests/sourceMapSupport.ts new file mode 100644 index 00000000000..475edf419f7 --- /dev/null +++ b/scripts/typemock/src/tests/sourceMapSupport.ts @@ -0,0 +1,3 @@ +import { install } from "source-map-support"; + +install(); \ No newline at end of file diff --git a/scripts/typemock/src/tests/stubTests.ts b/scripts/typemock/src/tests/stubTests.ts new file mode 100644 index 00000000000..53ea8da20c7 --- /dev/null +++ b/scripts/typemock/src/tests/stubTests.ts @@ -0,0 +1,79 @@ +import "./sourceMapSupport"; +import { Mock } from "../mock"; +import { Stub } from "../stub"; +import { Times } from "../times"; +import { assert } from "chai"; + +describe("stub", () => { + it("stub install replaces value", () => { + // arrange + const mock = new Mock({ a: 1 }); + const stub = new Stub(mock.value, "a", 2); + + // act + stub.install(); + + // assert + mock.verify(_ => _.a = 2, Times.once()); + }); + it("stub install is installed", () => { + // arrange + const mock = new Mock({ a: 1 }); + const stub = new Stub(mock.value, "a", 2); + + // act + stub.install(); + + // assert + assert.isTrue(stub.installed); + }); + it("stub install twice only installs once", () => { + // arrange + const mock = new Mock({ a: 1 }); + const stub = new Stub(mock.value, "a", 2); + + // act + stub.install(); + stub.install(); + + // assert + mock.verify(_ => _.a = 2, Times.once()); + }); + it("stub uninstall restores value", () => { + // arrange + const mock = new Mock({ a: 1 }); + const stub = new Stub(mock.value, "a", 2); + stub.install(); + + // act + stub.uninstall(); + + // assert + mock.verify(_ => _.a = 1, Times.once()); + }); + it("stub uninstall is not installed", () => { + // arrange + const mock = new Mock({ a: 1 }); + const stub = new Stub(mock.value, "a", 2); + stub.install(); + + // act + stub.uninstall(); + + // assert + assert.isFalse(stub.installed); + }); + it("stub uninstall twice only uninstalls once", () => { + // arrange + const mock = new Mock({ a: 1 }); + const stub = new Stub(mock.value, "a", 2); + stub.install(); + + // act + stub.uninstall(); + stub.uninstall(); + + // assert + mock.verify(_ => _.a = 1, Times.once()); + }); +}); diff --git a/scripts/typemock/src/tests/timersTests.ts b/scripts/typemock/src/tests/timersTests.ts new file mode 100644 index 00000000000..5d3b9d7917a --- /dev/null +++ b/scripts/typemock/src/tests/timersTests.ts @@ -0,0 +1,305 @@ +import "./sourceMapSupport"; +import { Spy } from "../spy"; +import { Arg } from "../arg"; +import { Times } from "../times"; +import { Timers } from "../timers"; +import { assert } from "chai"; + +describe("timers", () => { + describe("immediate", () => { + it("set adds entry, does not invoke", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.setImmediate(spy.value); + const pending = target.getPending(); + + // assert + assert.strictEqual(pending.length, 1); + assert.strictEqual(pending[0].kind, "immediate"); + assert.isDefined(handle); + spy.called(Times.none()); + }); + it("set/clear", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.setImmediate(spy.value); + target.clearImmedate(handle); + const pending = target.getPending(); + + // assert + assert.strictEqual(pending.length, 0); + spy.called(Times.none()); + }); + it("set one and execute", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.setImmediate(spy.value); + const count = target.executeImmediates(); + + // assert + assert.strictEqual(count, 1); + spy.called(Times.once()); + }); + it("set one with arg and execute", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.setImmediate(spy.value, "a"); + const count = target.executeImmediates(); + + // assert + assert.strictEqual(count, 1); + spy.verify(_ => _(Arg.typeof("string")), Times.once()); + }); + it("nested with maxDepth = 0", () => { + // arrange + const target = new Timers(); + const spy = new Spy(() => { target.setImmediate(spy.value); }); + + // act + target.setImmediate(spy.value); + const count = target.executeImmediates(/*maxDepth*/ 0); + + // assert + assert.strictEqual(count, 1); + spy.called(Times.once()); + }); + it("nested with maxDepth = 1", () => { + // arrange + const target = new Timers(); + const spy = new Spy(() => { target.setImmediate(spy.value); }); + + // act + target.setImmediate(spy.value); + const count = target.executeImmediates(/*maxDepth*/ 1); + + // assert + assert.strictEqual(count, 2); + spy.called(Times.exactly(2)); + }); + }); + describe("timeout", () => { + it("set adds entry, does not invoke", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.setTimeout(spy.value, 0); + const pending = target.getPending(); + + // assert + assert.strictEqual(pending.length, 1); + assert.strictEqual(pending[0].kind, "timeout"); + assert.isDefined(handle); + spy.called(Times.none()); + }); + it("set/clear", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.setTimeout(spy.value, 0); + target.clearTimeout(handle); + const pending = target.getPending(); + + // assert + assert.strictEqual(pending.length, 0); + spy.called(Times.none()); + }); + it("set adds future entry, advance prior to due does not invoke", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.setTimeout(spy.value, 10); + const count = target.advance(9); + + // assert + assert.strictEqual(count, 0); + spy.called(Times.none()); + }); + it("set adds future entry, advance to due invokes", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.setTimeout(spy.value, 10); + const count = target.advance(10); + + // assert + assert.strictEqual(count, 1); + spy.called(Times.once()); + }); + it("5 nested sets throttle", () => { + // arrange + const target = new Timers(); + const spy = new Spy(() => { target.setTimeout(spy.value, 0); }); + + // act + target.setTimeout(spy.value, 0); + const count = target.advance(1); + + // assert + assert.strictEqual(count, 5); + spy.called(Times.exactly(5)); + }); + }); + describe("interval", () => { + it("set adds entry, does not invoke", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.setInterval(spy.value, 0); + const pending = target.getPending({ kind: "interval", ms: 10 }); + + // assert + assert.strictEqual(pending.length, 1); + assert.strictEqual(pending[0].kind, "interval"); + assert.strictEqual(pending[0].interval, 10); + assert.isDefined(handle); + spy.called(Times.none()); + }); + it("set/clear", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.setInterval(spy.value, 0); + target.clearInterval(handle); + const pending = target.getPending({ kind: "interval", ms: 10 }); + + // assert + assert.strictEqual(pending.length, 0); + spy.called(Times.none()); + }); + it("set adds future entry, advance prior to due does not invoke", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.setInterval(spy.value, 10); + const count = target.advance(9); + + // assert + assert.strictEqual(count, 0); + spy.called(Times.none()); + }); + it("set adds future entry, advance to due invokes", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.setInterval(spy.value, 10); + const count = target.advance(10); + + // assert + assert.strictEqual(count, 1); + spy.called(Times.once()); + }); + it("set adds future entry, advance to due twice invokes twice", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.setInterval(spy.value, 10); + const count = target.advance(20); + + // assert + assert.strictEqual(count, 2); + spy.called(Times.exactly(2)); + }); + it("set adds future entry, remove before second due time", () => { + // arrange + const target = new Timers(); + const spy = new Spy(() => { target.clearInterval(handle); }); + + // act + const handle = target.setInterval(spy.value, 10); + const count = target.advance(20); + + // assert + assert.strictEqual(count, 1); + spy.called(Times.exactly(1)); + }); + }); + describe("frame", () => { + it("request adds entry, does not invoke", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.requestAnimationFrame(spy.value); + const pending = target.getPending({ ms: 16 }); + + // assert + assert.strictEqual(pending.length, 1); + assert.strictEqual(pending[0].kind, "frame"); + assert.isDefined(handle); + spy.called(Times.none()); + }); + it("request/cancel", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + const handle = target.requestAnimationFrame(spy.value); + target.cancelAnimationFrame(handle); + const pending = target.getPending(); + + // assert + assert.strictEqual(pending.length, 0); + spy.called(Times.none()); + }); + it("request and advance past one frame", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.requestAnimationFrame(spy.value); + const count = target.advance(16); + + // assert + assert.strictEqual(count, 1); + spy.called(Times.once()); + }); + it("requests clamped to 16ms", () => { + // arrange + const target = new Timers(); + const spy = new Spy(); + + // act + target.requestAnimationFrame(spy.value); + target.advance(10); + target.requestAnimationFrame(spy.value); + const count = target.advance(16); + + // assert + assert.strictEqual(count, 2); + spy.called(Times.exactly(2)); + }); + }); +}); \ No newline at end of file diff --git a/scripts/typemock/src/tests/timesTests.ts b/scripts/typemock/src/tests/timesTests.ts new file mode 100644 index 00000000000..4cce2500cb7 --- /dev/null +++ b/scripts/typemock/src/tests/timesTests.ts @@ -0,0 +1,236 @@ +import "./sourceMapSupport"; +import { Times } from "../times"; +import { theory, recordError } from "./utils"; +import { assert } from "chai"; + +describe("times", () => { + function makeTimesNoneValidationData(): any[][]{ + return [ + [0, true], + [1, false] + ]; + } + + theory("Times.none validation", makeTimesNoneValidationData, function (count: number, expected: boolean): void { + // arrange + const times = Times.none(); + + // act + const result = times.validate(count); + + // assert + assert.equal(expected, result); + }); + + function makeTimesOnceValidationData(): any[][]{ + return [ + [0, false], + [1, true], + [2, false] + ]; + } + + theory("Times.once validation", makeTimesOnceValidationData, function (count: number, expected: boolean): void { + // arrange + const times = Times.once(); + + // act + const result = times.validate(count); + + // assert + assert.equal(expected, result); + }); + + function makeTimesAtLeastOnceValidationData(): any[] { + return [ + [0, false], + [1, true], + [2, true] + ]; + } + + theory("Times.atLeastOnce validation", makeTimesAtLeastOnceValidationData, function (count: number, expected: boolean): void { + // arrange + const times = Times.atLeastOnce(); + + // act + const result = times.validate(count); + + // assert + assert.equal(expected, result); + }); + + function makeTimesAtMostOnceValidationData(): any[][]{ + return [ + [0, true], + [1, true], + [2, false] + ]; + } + + theory("Times.atMostOnce validation", makeTimesAtMostOnceValidationData, function (count: number, expected: boolean): void { + // arrange + const times = Times.atMostOnce(); + + // act + const result = times.validate(count); + + // assert + assert.equal(expected, result); + }); + + function makeTimesExactlyValidationData(): any[][]{ + return [ + [0, 0, true], + [0, 1, false], + [1, 0, false], + [1, 1, true]]; + } + + theory("Times.exactly validation", makeTimesExactlyValidationData, function (expectedCount: number, count: number, expectedResult: boolean): void { + // arrange + const times = Times.exactly(expectedCount); + + // act + const result = times.validate(count); + + // assert + assert.equal(expectedResult, result); + }); + + function makeTimesAtLeastValidationData(): any[][]{ + return [ + [0, 0, true], + [0, 1, true], + [1, 0, false], + [1, 1, true], + [1, 2, true] + ]; + } + + theory("Times.atLeast validation", makeTimesAtLeastValidationData, function (expectedCount: number, count: number, expectedResult: boolean): void { + // arrange + const times = Times.atLeast(expectedCount); + + // act + const result = times.validate(count); + + // assert + assert.equal(expectedResult, result); + }); + + function makeTimesAtMostValidationData(): any[][]{ + return [ + [0, 0, true], + [0, 1, false], + [1, 0, true], + [1, 1, true], + [1, 2, false] + ]; + } + + theory("Times.atMost validation", makeTimesAtMostValidationData, function (expectedCount: number, count: number, expectedResult: boolean): void { + // arrange + const times = Times.atMost(expectedCount); + + // act + const result = times.validate(count); + + // assert + assert.equal(expectedResult, result); + }); + + function makeTimesBetweenValidationData(): any[][]{ + return [ + [1, 2, 0, false], + [1, 2, 1, true], + [1, 2, 2, true], + [1, 2, 3, false] + ]; + } + + theory("Times.between validation", makeTimesBetweenValidationData, function (min: number, max: number, count: number, expectedResult: boolean): void { + // arrange + const times = Times.between(min, max); + + // act + const result = times.validate(count); + + // assert + assert.equal(expectedResult, result); + }); + + function makeTimesToStringData(): any[][]{ + return [ + [Times.none(), ""], + [Times.once(), ""], + [Times.atLeastOnce(), ""], + [Times.atMostOnce(), ""], + [Times.atLeast(2), ""], + [Times.atMost(2), ""], + [Times.exactly(2), ""], + [Times.between(1, 2), ""] + ]; + } + + theory("Times.toString", makeTimesToStringData, function (times: Times, expected: string): void { + // arrange + // act + const result = times.toString(); + + // assert + assert.equal(expected, result); + }); + + function makeTimesCheckThrowsData(): any[][]{ + return [ + [Times.none(), 1], + [Times.once(), 0], + [Times.once(), 2], + [Times.atLeastOnce(), 0], + [Times.atMostOnce(), 2], + [Times.atLeast(2), 1], + [Times.atMost(2), 3], + [Times.exactly(1), 0], + [Times.exactly(1), 2], + [Times.between(1, 2), 0], + [Times.between(1, 2), 3] + ] + } + + theory("Times.check throws", makeTimesCheckThrowsData, (times: Times, count: number) => { + // arrange + // act + const e = recordError(() => times.check(count, "test")); + + // assert + assert.instanceOf(e, Error); + }); + + function makeTimesCheckPassesData(): any[][] { + return [ + [Times.none(), 0], + [Times.once(), 1], + [Times.atLeastOnce(), 1], + [Times.atLeastOnce(), 2], + [Times.atMostOnce(), 1], + [Times.atMostOnce(), 0], + [Times.atLeast(2), 2], + [Times.atLeast(2), 3], + [Times.atMost(2), 2], + [Times.atMost(2), 1], + [Times.exactly(1), 1], + [Times.between(1, 2), 1], + [Times.between(1, 2), 2] + ]; + } + + theory("Times.check passes", makeTimesCheckPassesData, (times: Times, count: number) => { + // arrange + // act + const e = recordError(() => times.check(count, "test")); + + // assert + assert.isUndefined(e); + }); +}); \ No newline at end of file diff --git a/scripts/typemock/src/tests/utils.ts b/scripts/typemock/src/tests/utils.ts new file mode 100644 index 00000000000..d28423a2887 --- /dev/null +++ b/scripts/typemock/src/tests/utils.ts @@ -0,0 +1,17 @@ +export function theory(name: string, data: any[][] | (() => any[][]), callback: (...args: any[]) => any) { + describe(name, () => { + for (const row of typeof data === "function" ? data() : data) { + it(row.toString(), () => callback(...row)); + } + }); +} + +export function recordError(action: () => void): Error | undefined { + try { + action(); + return undefined; + } + catch (e) { + return e; + } +} \ No newline at end of file diff --git a/scripts/typemock/src/timers.ts b/scripts/typemock/src/timers.ts new file mode 100644 index 00000000000..7811290b6d3 --- /dev/null +++ b/scripts/typemock/src/timers.ts @@ -0,0 +1,475 @@ +export interface Immediate { + readonly kind: "immediate"; + readonly handle: number; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; +} + +export interface Timeout { + readonly kind: "timeout"; + readonly handle: number; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; +} + +export interface Interval { + readonly kind: "interval"; + readonly handle: number; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; + readonly interval: number; +} + +export interface AnimationFrame { + readonly kind: "frame"; + readonly handle: number; + readonly callback: (time: number) => void; +} + +export type Timer = Immediate | Timeout | Interval | AnimationFrame; + +type NonImmediateTimer = Timeout | Interval | AnimationFrame; + +interface Due { + timer: T; + due: number; + depth?: number; + enabled?: boolean; + timeline?: boolean; +} + +const MAX_INT32 = 2 ** 31 - 1; +const MIN_TIMEOUT_VALUE = 4; +const CLAMP_TIMEOUT_NESTING_LEVEL = 5; + +/** + * Programmatic control over timers. + */ +export class Timers { + public static readonly MAX_DEPTH = MAX_INT32; + + private _nextHandle = 1; + private _immediates = new Map>(); + private _timeouts = new Map>(); + private _intervals = new Map>(); + private _frames = new Map>(); + private _timeline: Due[] = []; + private _time: number; + private _depth = 0; + + constructor() { + this._time = 0; + + // bind each timer method so that it can be detached from this instance. + this.setImmediate = this.setImmediate.bind(this); + this.clearImmedate = this.clearImmedate.bind(this); + this.setTimeout = this.setTimeout.bind(this); + this.clearTimeout = this.clearTimeout.bind(this); + this.setInterval = this.setInterval.bind(this); + this.clearInterval = this.clearInterval.bind(this); + this.requestAnimationFrame = this.requestAnimationFrame.bind(this); + this.cancelAnimationFrame = this.cancelAnimationFrame.bind(this); + } + + /** + * Get the current time. + */ + public get time(): number { + return this._time; + } + + /** + * Gets the time of the last scheduled timer (not including repeating intervals). + */ + public get endTime(): number { + return this._timeline && this._timeline.length > 0 + ? this._timeline[this._timeline.length - 1].due + : this._time; + } + + /** + * Gets the estimated time remaining. + */ + public get remainingTime(): number { + return this.endTime - this.time; + } + + public getPending(options: { kind: "immediate", ms?: number }): Immediate[]; + public getPending(options: { kind: "timeout", ms?: number }): Timeout[]; + public getPending(options: { kind: "interval", ms?: number }): Interval[]; + public getPending(options: { kind: "frame", ms?: number }): AnimationFrame[]; + public getPending(options?: { kind?: Timer["kind"], ms?: number }): Timer[]; + public getPending(options: { kind?: Timer["kind"], ms?: number } = {}): Timer[] { + const { kind, ms = 0 } = options; + if (ms < 0) throw new TypeError("Argument 'ms' out of range."); + + const dueTimers: Due[] = []; + + if (!kind || kind === "immediate") { + this.copyImmediates(dueTimers); + } + + if (kind !== "immediate") { + this.copyTimelineBefore(dueTimers, this._time + ms, kind); + } + + return dueTimers.map(dueTimer => dueTimer.timer); + } + + /** + * Advance the current time and trigger callbacks, returning the number of callbacks triggered. + * @param ms The number of milliseconds to advance. + * @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing. + * - Use `0` (default) to disable processing of nested `setImmediate` calls. + * - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth. + */ + public advance(ms: number, maxDepth = 0): number { + if (ms <= 0) throw new TypeError("Argument 'ms' out of range."); + if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range."); + let count = 0; + const endTime = this._time + (ms | 0); + while (true) { + count += this.executeImmediates(maxDepth); + const dueTimer = this.dequeueIfBefore(endTime); + if (dueTimer) { + this._time = dueTimer.due; + this.executeTimer(dueTimer); + count++; + } + else { + this._time = endTime; + return count; + } + } + } + + /** + * Advance the current time to the estimated end time and trigger callbacks, returning the number of callbacks triggered. + * @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing. + * - Use `0` (default) to disable processing of nested `setImmediate` calls. + * - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth. + */ + public advanceToEnd(maxDepth = 0) { + return this.remainingTime > 0 ? this.advance(this.remainingTime, maxDepth) : 0; + } + + /** + * Execute any pending immediate timers, returning the number of timers triggered. + * @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing. + * - Use `0` (default) to disable processing of nested `setImmediate` calls. + * - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth. + */ + public executeImmediates(maxDepth = 0): number { + if ((maxDepth |= 0) < 0) throw new TypeError("Argument 'maxDepth' out of range."); + const dueTimers: Due[] = []; + this.copyImmediates(dueTimers); + let count = this.executeTimers(dueTimers); + for (let depth = 0; depth < maxDepth && this._immediates.size > 0; depth++) { + count += this.executeImmediates(); + } + return count; + } + + public setImmediate(callback: (...args: any[]) => void, ...args: any[]): any { + if (this._depth >= Timers.MAX_DEPTH) { + throw new Error("callback nested too deeply."); + } + + const timer: Immediate = { kind: "immediate", handle: this._nextHandle++, callback, args }; + const dueTimer: Due = { timer, due: -1 }; + this.addTimer(this._immediates, dueTimer); + return timer.handle; + } + + public clearImmedate(timerId: any): void { + const dueTimer = this._immediates.get(timerId); + if (dueTimer) { + this.deleteTimer(this._immediates, dueTimer); + } + } + + public setTimeout(callback: (...args: any[]) => void, timeout: number, ...args: any[]): any { + if (this._depth >= Timers.MAX_DEPTH) { + throw new Error("callback nested too deeply."); + } + + if ((timeout |= 0) < 0) timeout = 0; + + if (this._depth >= CLAMP_TIMEOUT_NESTING_LEVEL && timeout < MIN_TIMEOUT_VALUE) { + timeout = MIN_TIMEOUT_VALUE; + } + + const timer: Timeout = { kind: "timeout", handle: this._nextHandle++, callback, args }; + const dueTimer: Due = { timer, due: this._time + timeout }; + this.addTimer(this._timeouts, dueTimer); + this.addToTimeline(dueTimer); + return timer.handle; + } + + public clearTimeout(timerId: any): void { + const dueTimer = this._timeouts.get(timerId); + if (dueTimer) { + this.deleteTimer(this._timeouts, dueTimer); + this.removeFromTimeline(dueTimer); + } + } + + public setInterval(callback: (...args: any[]) => void, interval: number, ...args: any[]): any { + if (this._depth >= Timers.MAX_DEPTH) { + throw new Error("callback nested too deeply."); + } + + if ((interval |= 0) < 10) interval = 10; + const timer: Interval = { kind: "interval", handle: this._nextHandle++, callback, args, interval }; + const dueTimer: Due = { timer, due: this._time + interval }; + this.addTimer(this._intervals, dueTimer); + this.addToTimeline(dueTimer); + return timer.handle; + } + + public clearInterval(timerId: any): void { + const dueTimer = this._intervals.get(timerId); + if (dueTimer) { + this.deleteTimer(this._intervals, dueTimer); + this.removeFromTimeline(dueTimer); + } + } + + public requestAnimationFrame(callback: (time: number) => void): any { + if (this._depth >= Timers.MAX_DEPTH) { + throw new Error("callback nested too deeply."); + } + + const timer: AnimationFrame = { kind: "frame", handle: this._nextHandle++, callback }; + const dueTimer: Due = { timer, due: this.nextFrameDueTime() }; + this.addTimer(this._frames, dueTimer); + this.addToTimeline(dueTimer); + return timer.handle; + } + + public cancelAnimationFrame(timerId: any): void { + const dueTimer = this._frames.get(timerId); + if (dueTimer) { + this.deleteTimer(this._frames, dueTimer); + this.removeFromTimeline(dueTimer); + } + } + + private nextFrameDueTime() { + return this._time + this.nextFrameDelta(); + } + + private nextFrameDelta() { + return 16 - this._time % 16; + } + + private addTimer(timers: Map>, dueTimer: Due) { + if (dueTimer.enabled) return; + timers.set(dueTimer.timer.handle, dueTimer); + dueTimer.depth = this._depth + 1; + dueTimer.enabled = true; + } + + private deleteTimer(timers: Map>, dueTimer: Due) { + if (!dueTimer.enabled) return; + timers.delete(dueTimer.timer.handle); + dueTimer.enabled = false; + } + + private executeTimers(dueTimers: Due[]) { + let count = 0; + for (const dueTimer of dueTimers) { + this.executeTimer(dueTimer); + count++; + } + return count; + } + + private executeTimer(dueTimer: Due) { + switch (dueTimer.timer.kind) { + case "immediate": return this.executeImmediate(>dueTimer); + case "timeout": return this.executeTimeout(>dueTimer); + case "interval": return this.executeInterval(>dueTimer); + case "frame": return this.executeAnimationFrame(>dueTimer); + } + } + + private executeImmediate(dueTimer: Due) { + if (!dueTimer.enabled) return; + + this.deleteTimer(this._immediates, dueTimer); + this.executeCallback(dueTimer.depth, dueTimer.timer.callback, ...dueTimer.timer.args); + } + + private executeTimeout(dueTimer: Due) { + if (!dueTimer.enabled) return; + + this.deleteTimer(this._timeouts, dueTimer); + this.removeFromTimeline(dueTimer); + this.executeCallback(dueTimer.depth, dueTimer.timer.callback, ...dueTimer.timer.args); + } + + private executeInterval(dueTimer: Due) { + if (!dueTimer.enabled) return; + + this.removeFromTimeline(dueTimer); + this.executeCallback(dueTimer.depth, dueTimer.timer.callback, ...dueTimer.timer.args); + + if (dueTimer.enabled) { + dueTimer.due += dueTimer.timer.interval; + this.addToTimeline(dueTimer); + } + } + + private executeAnimationFrame(dueTimer: Due) { + if (!dueTimer.enabled) return; + + this.deleteTimer(this._frames, dueTimer); + this.removeFromTimeline(dueTimer); + this.executeCallback(dueTimer.depth, dueTimer.timer.callback, this._time); + } + + private executeCallback(depth = 0, callback: (...args: any[]) => void, ...args: any[]) { + const savedDepth = this._depth; + this._depth = depth; + try { + callback(...args); + } + finally { + this._depth = savedDepth; + } + } + + private dequeueIfBefore(dueTime: number) { + if (this._timeline.length > 0) { + const dueTimer = this._timeline[0]; + if (dueTimer.due <= dueTime) { + this._timeline.shift(); + dueTimer.timeline = false; + return dueTimer; + } + } + } + + private copyImmediates(dueTimers: Due[]) { + for (const dueTimer of this._immediates.values()) { + dueTimers.push(dueTimer); + } + } + + private copyTimelineBefore(dueTimers: Due[], dueTime: number, kind?: Timer["kind"]) { + for (const dueTimer of this._timeline) { + if (dueTimer.due <= dueTime && (!kind || dueTimer.timer.kind === kind)) { + dueTimers.push(dueTimer); + } + } + } + + private addToTimeline(dueTimer: Due) { + if (dueTimer.timeline) return; + + let index = binarySearch(this._timeline, dueTimer, getDueTime, compareTimestamps); + if (index < 0) { + index = ~index; + } + else { + while (index < this._timeline.length) { + if (this._timeline[index].due > dueTimer.due) { + break; + } + index++; + } + } + + insertAt(this._timeline, index, dueTimer); + dueTimer.timeline = true; + } + + private removeFromTimeline(dueTimer: Due) { + if (dueTimer.timeline) { + let index = binarySearch(this._timeline, dueTimer, getDueTime, compareTimestamps); + if (index >= 0) { + while (index < this._timeline.length) { + const event = this._timeline[index]; + if (event === dueTimer) { + removeAt(this._timeline, index); + dueTimer.timeline = false; + return true; + } + if (event.due > dueTimer.due) { + break; + } + index++; + } + } + } + return false; + } +} + +function getDueTime(v: Due) { + return v.due; +} + +function compareTimestamps(a: number, b: number) { + return a - b; +} + +function binarySearch(array: ReadonlyArray, value: T, keySelector: (v: T) => U, keyComparer: (a: U, b: U) => number): number { + if (array.length === 0) { + return -1; + } + + let low = 0; + let high = array.length - 1; + const key = keySelector(value); + while (low <= high) { + const middle = low + ((high - low) >> 1); + const midKey = keySelector(array[middle]); + const result = keyComparer(midKey, key); + if (result < 0) { + low = middle + 1; + } + else if (result > 0) { + high = middle - 1; + } + else { + return middle; + } + } + + return ~low; +} + +function removeAt(array: T[], index: number): void { + if (array.length === 0) { + return; + } + else if (index === 0) { + array.shift(); + } + else if (index === array.length - 1) { + array.pop(); + } + else { + for (let i = index; i < array.length - 1; i++) { + array[i] = array[i + 1]; + } + array.length--; + } +} + +function insertAt(array: T[], index: number, value: T): void { + 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; + } +} \ No newline at end of file diff --git a/scripts/typemock/src/times.ts b/scripts/typemock/src/times.ts new file mode 100644 index 00000000000..318d4702dc4 --- /dev/null +++ b/scripts/typemock/src/times.ts @@ -0,0 +1,120 @@ +/** + * Defines the number of times an action must have been executed during verification of a Mock. + */ +export class Times { + private static _none: Times | undefined; + private static _once: Times | undefined; + private static _atLeastOnce: Times | undefined; + private static _atMostOnce: Times | undefined; + + private _min: number; + private _max: number; + private _message: string; + + private constructor(min: number, max: number, message: string) { + this._min = min; + this._max = max; + this._message = message; + } + + /** + * Expects that an action was never executed. + * @returns A new `Times` instance. + */ + public static none(): Times { + return this._none || (this._none = new Times(0, 0, `never`)); + } + + /** + * Expects that an action was executed exactly once. + * @returns A new `Times` instance. + */ + public static once(): Times { + return this._once || (this._once = new Times(1, 1, `exactly once`)); + } + + /** + * Expects that an action was executed at least once. + * @returns A new `Times` instance. + */ + public static atLeastOnce(): Times { + return this._atLeastOnce || (this._atLeastOnce = new Times(1, Number.MAX_SAFE_INTEGER, `at least once`)); + } + + /** + * Expects that an action was executed at least the specified number of times. + * @param count The number of times. + * @returns A new `Times` instance. + */ + public static atLeast(count: number): Times { + return new Times(count, Number.MAX_SAFE_INTEGER, `at least ${count} time(s)`); + } + + /** + * Expects that an action was executed exactly the specified number of times. + * @param count The number of times. + * @returns A new `Times` instance. + */ + public static exactly(count: number): Times { + return new Times(count, count, `exactly ${count} time(s)`); + } + + /** + * Expects that an action was executed at most the specified number of times. + * @param count The number of times. + * @returns A new `Times` instance. + */ + public static atMost(count: number): Times { + return new Times(0, count, `at most ${count} time(s)`); + } + + /** + * Expects that an action was executed at most once. + * @returns A new `Times` instance. + */ + public static atMostOnce(): Times { + return this._atMostOnce || (this._atMostOnce = new Times(0, 1, `at most once`)); + } + + /** + * Expects that an action was executed between a range of times, inclusive. + * @param min The minimum number of times, inclusive. + * @param max The maximum number of times, inclusive. + * @returns A new `Times` instance. + */ + public static between(min: number, max: number): Times { + return new Times(min, max, `between ${min} and ${max} time(s)`); + } + + /** + * Validates the number of times an action was executed. + * @param count The number of times the action was executed. + * @returns `true` if the provided count was valid; otherwise, `false`. + */ + public validate(count: number): boolean { + if (count < this._min) return false; + if (count > this._max) return false; + return true; + } + + /** + * Checks the number of times an action was executed, throwing an error if the count was not valid. + * @param count The number of times the action was executed. + * @param message The message to use to begin the check. + */ + public check(count: number, message: string): void { + if (!this.validate(count)) { + const expectedMessage = this._message === `never` + ? `Expected to never be executed.` + : `Expected to be executed ${this._message}.`; + throw new Error(`${message}\n${expectedMessage} Actually executed ${count} time(s).`); + } + } + + /** + * Gets the string representation of this object. + */ + public toString(): string { + return `<${this._message}>`; + } +} diff --git a/scripts/typemock/src/tsconfig.json b/scripts/typemock/src/tsconfig.json new file mode 100644 index 00000000000..23d6c614dbc --- /dev/null +++ b/scripts/typemock/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "strict": true, + "declaration": true, + "sourceMap": true, + "types": ["mocha"] + } +} \ No newline at end of file diff --git a/src/harness/core.ts b/src/harness/core.ts index 10e5bb7a11b..94988774955 100644 --- a/src/harness/core.ts +++ b/src/harness/core.ts @@ -191,6 +191,10 @@ namespace core { } } + public [Symbol.iterator]() { + return this.entries(); + } + private writePreamble() { if (this._copyOnWrite) { this._keys = this._keys.slice(); @@ -211,6 +215,109 @@ namespace core { } } + export class SortedSet implements ReadonlySet { + private _comparer: (a: T, b: T) => number; + private _values: T[] = []; + private _version = 0; + private _copyOnWrite = false; + + constructor(comparer: (a: T, b: T) => number) { + this._comparer = comparer; + } + + public get size() { + return this._values.length; + } + + public has(value: T) { + return binarySearch(this._values, value, identity, this._comparer) >= 0; + } + + public add(value: T) { + const index = binarySearch(this._values, value, identity, this._comparer); + if (index < 0) { + this.writePreamble(); + insertAt(this._values, ~index, value); + this.writePostScript(); + } + return this; + } + + public delete(value: T) { + const index = binarySearch(this._values, value, identity, this._comparer); + if (index >= 0) { + this.writePreamble(); + removeAt(this._values, index); + this.writePostScript(); + return true; + } + return false; + } + + public clear() { + if (this.size > 0) { + this.writePreamble(); + this._values.length = 0; + this.writePostScript(); + } + } + + public forEach(callback: (value: T, key: T, collection: this) => void) { + const values = this._values; + const version = this._version; + this._copyOnWrite = true; + for (const value of values) { + callback(value, value, this); + } + if (version === this._version) { + this._copyOnWrite = false; + } + } + + public keys() { + return this.values(); + } + + public * values() { + const values = this._values; + const version = this._version; + this._copyOnWrite = true; + for (const value of values) { + yield value; + } + if (version === this._version) { + this._copyOnWrite = false; + } + } + + public * entries() { + const values = this._values; + const version = this._version; + this._copyOnWrite = true; + for (const value of values) { + yield [value, value] as [T, T]; + } + if (version === this._version) { + this._copyOnWrite = false; + } + } + + public [Symbol.iterator]() { + return this.values(); + } + + private writePreamble() { + if (this._copyOnWrite) { + this._values = this._values.slice(); + this._copyOnWrite = false; + } + } + + private writePostScript() { + this._version++; + } + } + /** * A collection of metadata that supports inheritance. */ @@ -314,11 +421,21 @@ namespace core { } export function removeAt(array: T[], index: number): void { - for (let i = index; i < array.length - 1; i++) { - array[i] = array[i + 1]; + if (index < 0 || index >= array.length) { + return; + } + else if (index === 0) { + array.shift(); + } + else if (index === array.length - 1) { + array.pop(); + } + else { + for (let i = index; i < array.length - 1; i++) { + array[i] = array[i + 1]; + } + array.length--; } - - array.length--; } export function insertAt(array: T[], index: number, value: T): void { diff --git a/src/harness/mocks.ts b/src/harness/mocks.ts index 315677b7117..97ea4b1e0ee 100644 --- a/src/harness/mocks.ts +++ b/src/harness/mocks.ts @@ -1,209 +1,98 @@ /// -/// import { debug } from "util"; - - +/// +/// +/// // NOTE: The contents of this file are all exported from the namespace 'mocks'. This is to // support the eventual conversion of harness into a modular system. +// harness mocks namespace mocks { - const MAX_INT32 = 2 ** 31 - 1; - - export interface Immediate { - readonly kind: "immediate"; - readonly callback: (...args: any[]) => void; - readonly args: ReadonlyArray; - } - - export interface Timeout { - readonly kind: "timeout"; - readonly callback: (...args: any[]) => void; - readonly args: ReadonlyArray; - readonly due: number; - } - - export interface Interval { - readonly kind: "interval"; - readonly callback: (...args: any[]) => void; - readonly args: ReadonlyArray; - readonly due: number; - readonly interval: number; - } - - export type Timer = Immediate | Timeout | Interval; - - interface InternalInterval extends Interval { - due: number; - } - - /** - * Programmatic control over timers. - */ - export class Timers { - public static readonly MAX_DEPTH = MAX_INT32; - private _immediates = new Set(); - private _timeouts = new Set(); - private _intervals = new Set(); - private _time: number; - - constructor(startTime = Date.now()) { - this._time = startTime; - - // bind each timer method so that it can be detached from this instance. - this.setImmediate = this.setImmediate.bind(this); - this.clearImmedate = this.clearImmedate.bind(this); - this.setTimeout = this.setTimeout.bind(this); - this.clearTimeout = this.clearImmedate.bind(this); - this.setInterval = this.setInterval.bind(this); - this.clearInterval = this.clearInterval.bind(this); - } - + export interface MockServerHostOptions { /** - * Get the current time. + * The `VirtualFleSystem` to use. If not specified, a new case-sensitive `VirtualFileSystem` + * is created. */ - public get time(): number { - return this._time; - } - - public getPending(kind: "immediate", ms?: number): Immediate[]; - public getPending(kind: "timeout", ms?: number): Timeout[]; - public getPending(kind: "interval", ms?: number): Interval[]; - public getPending(kind?: Timer["kind"], ms?: number): Timer[]; - public getPending(kind?: Timer["kind"], ms = 0) { - if (ms < 0) throw new TypeError("Argument 'ms' out of range."); - const pending: Timer[] = []; - if (!kind || kind === "immediate") this.appendImmediates(pending); - if (!kind || kind === "timeout") this.appendDueTimeouts(pending, this._time + ms); - if (!kind || kind === "interval") this.appendDueIntervals(pending, this._time + ms, /*expand*/ false); - return core.stableSort(pending, compareTimers); - } - + vfs?: vfs.VirtualFileSystem | { currentDirectory?: string, useCaseSensitiveFileNames?: boolean }; /** - * Advance the current time and trigger callbacks, returning the number of callbacks triggered. - * @param ms The number of milliseconds to advance. - * @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing. - * - Use `0` (default) to disable processing of nested `setImmediate` calls. - * - Use `Timer.NO_MAX_DEPTH` to continue processing all nested `setImmediate` calls. + * The virtual path to tsc.js. If not specified, a default of `"/.ts/tsc.js"` is used. */ - public advance(ms: number, maxDepth = 0): number { - if (ms < 0) throw new TypeError("Argument 'ms' out of range."); - if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range."); - this._time += ms; - return this.executePending(maxDepth); - } - + executingFilePath?: string; /** - * Execute any pending timers, returning the number of timers triggered. - * @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing. - * - Use `0` (default) to disable processing of nested `setImmediate` calls. - * - Use `Timer.NO_MAX_DEPTH` to continue processing all nested `setImmediate` calls. + * The new-line style. If not specified, a default of `"\n"` is used. */ - public executePending(maxDepth = 0): number { - if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range."); - const pending: Timer[] = []; - this.appendImmediates(pending); - this.appendDueTimeouts(pending, this._time); - this.appendDueIntervals(pending, this._time, /*expand*/ true); - let count = this.execute(pending); - for (let depth = 0; depth < maxDepth && this._immediates.size > 0; depth++) { - pending.length = 0; - this.appendImmediates(pending); - count += this.execute(pending); - } - return count; - } - - public setImmediate(callback: (...args: any[]) => void, ...args: any[]): any { - const timer: Immediate = { kind: "immediate", callback, args }; - this._immediates.add(timer); - return timer; - } - - public clearImmedate(timerId: any): void { - this._immediates.delete(timerId); - } - - public setTimeout(callback: (...args: any[]) => void, timeout: number, ...args: any[]): any { - if (timeout < 0) timeout = 0; - const due = this._time + timeout; - const timer: Timeout = { kind: "timeout", callback, args, due }; - this._timeouts.add(timer); - return timer; - } - - public clearTimeout(timerId: any): void { - this._timeouts.delete(timerId); - } - - public setInterval(callback: (...args: any[]) => void, interval: number, ...args: any[]): any { - if (interval < 0) interval = 0; - const due = this._time + interval; - const timer: Interval = { kind: "interval", callback, args, due, interval }; - this._intervals.add(timer); - return timer; - } - - public clearInterval(timerId: any): void { - this._intervals.delete(timerId); - } - - private appendImmediates(pending: Timer[]) { - this._immediates.forEach(timer => { - pending.push(timer); - }); - } - - private appendDueTimeouts(timers: Timer[], dueTime: number) { - this._timeouts.forEach(timer => { - if (timer.due <= dueTime) { - timers.push(timer); - } - }); - } - - private appendDueIntervals(timers: Timer[], dueTime: number, expand: boolean) { - this._intervals.forEach(timer => { - while (timer.due <= dueTime) { - timers.push(timer); - if (!expand) break; - timer.due += timer.interval; - } - }); - } - - private execute(timers: Timer[]) { - for (const timer of core.stableSort(timers, compareTimers)) { - switch (timer.kind) { - case "immediate": this._immediates.delete(timer); break; - case "timeout": this._timeouts.delete(timer); break; - } - const { callback, args } = timer; - callback(...args); - } - return timers.length; - } - } - - function compareTimers(a: Immediate | Timeout, b: Immediate | Timeout) { - return (a.kind === "immediate" ? -1 : a.due) - (b.kind === "immediate" ? -1 : b.due); + newLine?: "\r\n" | "\n"; + /** + * Indicates whether to include _safeList.json_. + */ + safeList?: boolean; + /** + * Indicates whether to include a bare _lib.d.ts_. + */ + lib?: boolean; } export class MockServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost { - public readonly exitMessage = "System Exit"; - public readonly timers = new Timers(); + public static readonly defaultExecutingFilePath = "/.ts/tsc.js"; + public static readonly defaultCurrentDirectory = "/"; + public static readonly safeListPath = "/safelist.json"; + public static readonly safeListContent = + `{\n` + + ` "commander": "commander",\n` + + ` "express": "express",\n` + + ` "jquery": "jquery",\n` + + ` "lodash": "lodash",\n` + + ` "moment": "moment",\n` + + ` "chroma": "chroma-js"\n` + + `}`; + + public static readonly libPath = "/.ts/lib.d.ts"; + public static readonly libContent = + `/// \n` + + `interface Boolean {}\n` + + `interface Function {}\n` + + `interface IArguments {}\n` + + `interface Number { toExponential: any; }\n` + + `interface Object {}\n` + + `interface RegExp {}\n` + + `interface String { charAt: any; }\n` + + `interface Array {}`; + + public readonly timers = new typemock.Timers(); public readonly vfs: vfs.VirtualFileSystem; public exitCode: number; + private static readonly processExitSentinel = new Error("System exit"); private readonly _output: string[] = []; private readonly _executingFilePath: string; private readonly _getCanonicalFileName: (file: string) => string; - constructor(vfs: vfs.VirtualFileSystem, executingFilePath = "/.ts/tsc.js", newLine = "\n") { - this.vfs = vfs; - this.useCaseSensitiveFileNames = vfs.useCaseSensitiveFileNames; + constructor(options: MockServerHostOptions = {}) { + const { + vfs: _vfs = {}, + executingFilePath = MockServerHost.defaultExecutingFilePath, + newLine = "\n", + safeList = false, + lib = false + } = options; + + const { currentDirectory = MockServerHost.defaultCurrentDirectory, useCaseSensitiveFileNames = false } = _vfs; + + this.vfs = _vfs instanceof vfs.VirtualFileSystem ? _vfs : + new vfs.VirtualFileSystem(currentDirectory, useCaseSensitiveFileNames); + + this.useCaseSensitiveFileNames = this.vfs.useCaseSensitiveFileNames; this.newLine = newLine; this._executingFilePath = executingFilePath; this._getCanonicalFileName = ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames); + + if (safeList) { + this.vfs.addFile(MockServerHost.safeListPath, MockServerHost.safeListContent); + } + + if (lib) { + this.vfs.addFile(MockServerHost.libPath, MockServerHost.libContent); + } } // #region DirectoryStructureHost members @@ -218,8 +107,8 @@ namespace mocks { return this.vfs.readFile(path); } - public writeFile(path: string, data: string): void { - this.vfs.writeFile(path, data); + public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void { + this.vfs.writeFile(path, writeByteOrderMark ? core.addUTF8ByteOrderMark(data) : data); } public fileExists(path: string) { @@ -250,7 +139,7 @@ namespace mocks { public exit(exitCode?: number) { this.exitCode = exitCode; - throw new Error("System exit"); + throw MockServerHost.processExitSentinel; } // #endregion DirectoryStructureHost members @@ -336,5 +225,26 @@ namespace mocks { public clearOutput() { this._output.length = 0; } + + public checkTimeoutQueueLength(expected: number) { + const callbacksCount = this.timers.getPending({ kind: "timeout", ms: this.timers.remainingTime }).length; + assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); + } + + public checkTimeoutQueueLengthAndRun(count: number) { + this.checkTimeoutQueueLength(count); + this.runQueuedTimeoutCallbacks(); + } + + public runQueuedTimeoutCallbacks() { + try { + this.timers.advanceToEnd(); + } + catch (e) { + if (e !== MockServerHost.processExitSentinel) { + throw e; + } + } + } } } \ No newline at end of file diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 11e03438090..06bc299d598 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -76,6 +76,7 @@ "core.ts", "utils.ts", + "typemock.ts", "events.ts", "documents.ts", "vpath.ts", diff --git a/src/harness/typemock.ts b/src/harness/typemock.ts new file mode 100644 index 00000000000..32950dfac74 --- /dev/null +++ b/src/harness/typemock.ts @@ -0,0 +1,98 @@ +/// +/// +/// + +// NOTE: The contents of this file are all exported from the namespace 'typemock'. This is to +// support the eventual conversion of harness into a modular system. + +// typemock library +namespace typemock { + type Imported = T["prototype"]; + + function unwrap(module: any, _: () => PromiseLike): T { return module; } + + const module = unwrap(require("../../scripts/typemock"), () => import("../../scripts/typemock")); + + export const Arg = module.Arg; + + export interface Arg extends Imported { + } + + export interface Returns { + returns: U; + } + + export interface Throws { + throws: any; + } + + export const Mock = module.Mock; + + export interface Mock extends Imported { + readonly value: T; + setup(callback: (value: T) => U, result?: Returns | Throws): Mock; + setup(setups: Partial): Mock; + verify(callback: (value: T) => any, times: Times): Mock; + } + + export type Callable = ((...args: any[]) => any); + + export type Constructable = (new (...args: any[]) => any); + + export const Spy = module.Spy; + + export interface Spy extends Imported { + readonly value: T; + verify(callback: (value: T) => any, times: Times): this; + } + + export const Times = module.Times; + + export interface Times extends Imported { + } + + export interface Immediate { + readonly kind: "immediate"; + readonly handle: number; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; + } + + export interface Timeout { + readonly kind: "timeout"; + readonly handle: number; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; + } + + export interface Interval { + readonly kind: "interval"; + readonly handle: number; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; + readonly interval: number; + } + + export interface AnimationFrame { + readonly kind: "frame"; + readonly handle: number; + readonly callback: (time: number) => void; + } + + export declare type Timer = Immediate | Timeout | Interval | AnimationFrame; + + export const Timers = module.Timers; + + export interface Timers extends Imported { + } + + export const Stub = module.Stub; + + export interface Stub extends Imported { + readonly target: T; + readonly key: K; + stubValue: T[K]; + readonly originalValue: T[K]; + readonly currentValue: T[K]; + } +} \ No newline at end of file diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index 36f9d1c366b..12fc8127732 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -1,10 +1,10 @@ /// /// /// +/// /// namespace ts.projectSystem { - import CommandNames = server.CommandNames; const nullCancellationToken = server.nullCancellationToken; function createTestTypingsInstaller(host: server.ServerHost) { @@ -12,26 +12,39 @@ namespace ts.projectSystem { } describe("CompileOnSave affected list", () => { - function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) { - const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; - const actualResult = response.sort((list1, list2) => ts.compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); - expectedFileList = expectedFileList.sort((list1, list2) => ts.compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); + interface ProjectFileList { + projectFileName: string; + files: (string | FileOrFolder | vfs.VirtualFile)[]; + } - assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`); + function sendAffectedFileRequestAndCheckResult(session: server.Session, args: server.protocol.FileRequestArgs, expectedFileList: ProjectFileList | ProjectFileList[]) { + checkAffectedFiles(sendCompileOnSaveAffectedFileListRequest(session, args), expectedFileList); + } - for (let i = 0; i < actualResult.length; i++) { - const actualResultSingleProject = actualResult[i]; + function checkAffectedFiles(actualFileList: server.protocol.CompileOnSaveAffectedFileListSingleProject[], expectedFileList: ProjectFileList | ProjectFileList[]) { + actualFileList = sort(actualFileList, compareFileLists); + expectedFileList = Array.isArray(expectedFileList) ? sort(expectedFileList, compareFileLists) : [expectedFileList]; + + assert.equal(actualFileList.length, expectedFileList.length, `Actual result project number is different from the expected project number`); + + for (let i = 0; i < actualFileList.length; i++) { + const actualResultSingleProject = actualFileList[i]; const expectedResultSingleProject = expectedFileList[i]; assert.equal(actualResultSingleProject.projectFileName, expectedResultSingleProject.projectFileName, `Actual result contains different projects than the expected result`); - const actualResultSingleProjectFileNameList = actualResultSingleProject.fileNames.sort(); - const expectedResultSingleProjectFileNameList = map(expectedResultSingleProject.files, f => f.path).sort(); + const actualResultSingleProjectFileNameList = sort(actualResultSingleProject.fileNames, compareStringsCaseSensitive); + const expectedFiles = map(expectedResultSingleProject.files, file => typeof file === "string" ? file : file.path); + const expectedResultSingleProjectFileNameList = sort(expectedFiles, compareStringsCaseSensitive); assert.isTrue( arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList), `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`); } } + function compareFileLists(a: T, b: T) { + return ts.compareStringsCaseSensitive(a.projectFileName, b.projectFileName); + } + function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller): server.Session { const opts: server.SessionOptions = { host, @@ -48,70 +61,26 @@ namespace ts.projectSystem { } describe("for configured projects", () => { - let moduleFile1: FileOrFolder; - let file1Consumer1: FileOrFolder; - let file1Consumer2: FileOrFolder; - let moduleFile2: FileOrFolder; - let globalFile3: FileOrFolder; - let configFile: FileOrFolder; - let changeModuleFile1ShapeRequest1: server.protocol.Request; - let changeModuleFile1InternalRequest1: server.protocol.Request; - // A compile on save affected file request using file1 - let moduleFile1FileListRequest: server.protocol.Request; + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); - let sharedFs: vfs.VirtualFileSystem; - before(() => { - const fs = new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true); - fs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); - fs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); - fs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); - fs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); - fs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - fs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); - fs.addFile(libFile.path, libFile.content); - fs.makeReadOnly(); - sharedFs = fs; - }); + const session = createSession(host, createTestTypingsInstaller(host)); - after(() => { - sharedFs = undefined; - }); + openFilesForSession([moduleFile1.path, file1Consumer1.path], session); - beforeEach(() => { - moduleFile1 = { - path: "/a/b/moduleFile1.ts", - content: "export function Foo() { };" - }; + // Send an initial compileOnSave request + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1.path, file1Consumer1.path, file1Consumer2.path] }); - file1Consumer1 = { - path: "/a/b/file1Consumer1.ts", - content: `import {Foo} from "./moduleFile1"; export var y = 10;` - }; - - file1Consumer2 = { - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;` - }; - - moduleFile2 = { - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;` - }; - - globalFile3 = { - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }; - - configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compileOnSave": true - }` - }; - - // Change the content of file1 to `export var T: number;export function Foo() { };` - changeModuleFile1ShapeRequest1 = makeSessionRequest(CommandNames.Change, { + sendChangeRequest(session, { file: moduleFile1.path, line: 1, offset: 1, @@ -120,8 +89,226 @@ namespace ts.projectSystem { insertString: `export var T: number;` }); - // Change the content of file1 to `export var T: number;export function Foo() { };` - changeModuleFile1InternalRequest1 = makeSessionRequest(CommandNames.Change, { + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + + // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };` + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 46, + endLine: 1, + endOffset: 46, + insertString: `console.log('hi');` + }); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1] }); + }); + + it("should be up-to-date with the reference map changes", () => { + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); + + const session = createSession(host, createTestTypingsInstaller(host)); + + openFilesForSession([moduleFile1.path, file1Consumer1.path], session); + + // Send an initial compileOnSave request + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + + // Change file2 content to `let y = Foo();` + sendChangeRequest(session, { + file: file1Consumer1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 28, + insertString: "" + }); + + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` + }); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }); + + // Add the import statements back to file2 + sendChangeRequest(session, { + file: file1Consumer1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `import {Foo} from "./moduleFile1";` + }); + + // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };` + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T2: string;` + }); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + }); + + it("should be up-to-date with changes made in non-open files", () => { + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); + + const session = createSession(host, createTestTypingsInstaller(host)); + + openFilesForSession([moduleFile1], session); + + // Send an initial compileOnSave request + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + + host.vfs.writeFile(file1Consumer1.path, `let y = 10;`); + + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` + }); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }); + }); + + it("should be up-to-date with deleted files", () => { + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); + + const session = createSession(host, createTestTypingsInstaller(host)); + + openFilesForSession([moduleFile1], session); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` + }); + + host.vfs.removeFile(file1Consumer2.path); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }); + }); + + it("should be up-to-date with newly created files", () => { + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); + + const session = createSession(host, createTestTypingsInstaller(host)); + + openFilesForSession([moduleFile1], session); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + + const file1Consumer3 = host.vfs.addFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); + + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` + }); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }); + }); + + it("should detect changes in non-root files", () => { + const host = new mocks.MockServerHost(); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "compileOnSave": true, "files": ["${file1Consumer1.path}"] }`); + host.vfs.addFile(libFile.path, libFile.content); + + const session = createSession(host, createTestTypingsInstaller(host)); + + openFilesForSession([moduleFile1, file1Consumer1], session); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }); + + // change file1 shape now, and verify both files are affected + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` + }); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }); + + // change file1 internal, and verify only file1 is affected + sendChangeRequest(session, { file: moduleFile1.path, line: 1, offset: 1, @@ -130,175 +317,27 @@ namespace ts.projectSystem { insertString: `var T1: number;` }); - moduleFile1FileListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path, projectFileName: configFile.path }); - }); - - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const host = new mocks.MockServerHost(sharedFs.shadow()); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };` - const changeFile1InternalRequest = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 46, - endLine: 1, - endOffset: 46, - insertString: `console.log('hi');` - }); - session.executeCommand(changeFile1InternalRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); - }); - - it("should be up-to-date with the reference map changes", () => { - const host = new mocks.MockServerHost(sharedFs.shadow()); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - // Change file2 content to `let y = Foo();` - const removeFile1Consumer1ImportRequest = makeSessionRequest(CommandNames.Change, { - file: file1Consumer1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 28, - insertString: "" - }); - session.executeCommand(removeFile1Consumer1ImportRequest); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); - - // Add the import statements back to file2 - const addFile2ImportRequest = makeSessionRequest(CommandNames.Change, { - file: file1Consumer1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `import {Foo} from "./moduleFile1";` - }); - session.executeCommand(addFile2ImportRequest); - - // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };` - const changeModuleFile1ShapeRequest2 = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T2: string;` - }); - session.executeCommand(changeModuleFile1ShapeRequest2); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - }); - - it("should be up-to-date with changes made in non-open files", () => { - const host = new mocks.MockServerHost(sharedFs.shadow()); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - host.vfs.writeFile(file1Consumer1.path, `let y = 10;`); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); - }); - - it("should be up-to-date with deleted files", () => { - const host = new mocks.MockServerHost(sharedFs.shadow()); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - session.executeCommand(changeModuleFile1ShapeRequest1); - - host.vfs.removeFile(file1Consumer2.path); - - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); - }); - - it("should be up-to-date with newly created files", () => { - const host = new mocks.MockServerHost(sharedFs.shadow()); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - const file1Consumer3: FileOrFolder = { - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; - host.vfs.writeFile(file1Consumer3.path, file1Consumer3.content); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]); - }); - - it("should detect changes in non-root files", () => { - moduleFile1 = { - path: "/a/b/moduleFile1.ts", - content: "export function Foo() { };" - }; - - file1Consumer1 = { - path: "/a/b/file1Consumer1.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; - - configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compileOnSave": true, - "files": ["${file1Consumer1.path}"] - }` - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(moduleFile1.path, moduleFile1.content); - host.vfs.addFile(file1Consumer1.path, file1Consumer1.content); - host.vfs.addFile(configFile.path, configFile.content); - host.vfs.addFile(libFile.path, libFile.content); - - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); - - // change file1 shape now, and verify both files are affected - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); - - // change file1 internal, and verify only file1 is affected - session.executeCommand(changeModuleFile1InternalRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1] }); }); it("should return all files if a global file changed shape", () => { - const host = new mocks.MockServerHost(sharedFs.shadow()); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + const globalFile3 = host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + const moduleFile2 = host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); + + const session = createSession(host, createTestTypingsInstaller(host)); openFilesForSession([globalFile3], session); - const changeGlobalFile3ShapeRequest = makeSessionRequest(CommandNames.Change, { + + // check after file1 shape changes + sendChangeRequest(session, { file: globalFile3.path, line: 1, offset: 1, @@ -307,74 +346,62 @@ namespace ts.projectSystem { insertString: `var T2: string;` }); - // check after file1 shape changes - session.executeCommand(changeGlobalFile3ShapeRequest); - const globalFile3FileListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path }); - sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }]); + sendAffectedFileRequestAndCheckResult(session, + { file: globalFile3.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }); }); it("should return empty array if CompileOnSave is not enabled", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{}`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); + + const session = createSession(host, createTestTypingsInstaller(host)); - const host = new mocks.MockServerHost(sharedFs.shadow()); - host.vfs.writeFile(configFile.path, configFile.content); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + []); }); it("should save when compileOnSave is enabled in base tsconfig.json", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "extends": "/a/tsconfig.json" - }` - }; + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "extends": "/a/tsconfig.json" }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); + host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); + host.vfs.addFile(libFile.path, libFile.content); + host.vfs.addFile("/a/tsconfig.json", `{ "compileOnSave": true }`); - const configFile2: FileOrFolder = { - path: "/a/tsconfig.json", - content: `{ - "compileOnSave": true - }` - }; - - const host = new mocks.MockServerHost(sharedFs.shadow()); - host.vfs.writeFile(configFile.path, configFile.content); - host.vfs.addFile(configFile2.path, configFile2.content); - - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); + const session = createSession(host, createTestTypingsInstaller(host)); openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }); }); it("should always return the file itself if '--isolatedModules' is specified", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compileOnSave": true, - "compilerOptions": { - "isolatedModules": true - } - }` - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(moduleFile1.path, moduleFile1.content); - host.vfs.addFile(file1Consumer1.path, file1Consumer1.content); - host.vfs.addFile(configFile.path, configFile.content); + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "compileOnSave": true, "compilerOptions": { "isolatedModules": true } }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); host.vfs.addFile(libFile.path, libFile.content); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); + const session = createSession(host, createTestTypingsInstaller(host)); + openFilesForSession([moduleFile1], session); - const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + sendChangeRequest(session, { file: moduleFile1.path, line: 1, offset: 27, @@ -382,33 +409,24 @@ namespace ts.projectSystem { endOffset: 27, insertString: `Point,` }); - session.executeCommand(file1ChangeShapeRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1] }); }); it("should always return the file itself if '--out' or '--outFile' is specified", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compileOnSave": true, - "compilerOptions": { - "module": "system", - "outFile": "/a/b/out.js" - } - }` - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(moduleFile1.path, moduleFile1.content); - host.vfs.addFile(file1Consumer1.path, file1Consumer1.content); - host.vfs.addFile(configFile.path, configFile.content); + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "compileOnSave": true, "compilerOptions": { "module": "system", "outFile": "/a/b/out.js" } }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); host.vfs.addFile(libFile.path, libFile.content); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); + const session = createSession(host, createTestTypingsInstaller(host)); + openFilesForSession([moduleFile1], session); - const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + sendChangeRequest(session, { file: moduleFile1.path, line: 1, offset: 27, @@ -416,32 +434,39 @@ namespace ts.projectSystem { endOffset: 27, insertString: `Point,` }); - session.executeCommand(file1ChangeShapeRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1] }); }); it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: FileOrFolder = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(moduleFile1.path, moduleFile1.content); - host.vfs.addFile(file1Consumer1.path, file1Consumer1.content); - host.vfs.addFile(file1Consumer1Consumer1.path, file1Consumer1Consumer1.content); - host.vfs.addFile(globalFile3.path, globalFile3.content); - host.vfs.addFile(configFile.path, configFile.content); + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); + const file1Consumer1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1Consumer1.ts", `import {y} from "./file1Consumer1";`); + host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile(libFile.path, libFile.content); - host.vfs.addFile(file1Consumer1Consumer1.path, file1Consumer1Consumer1.content); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); + const session = createSession(host, createTestTypingsInstaller(host)); openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); - const changeFile1Consumer1ShapeRequest = makeSessionRequest(CommandNames.Change, { + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }); + + sendChangeRequest(session, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` + }); + + sendChangeRequest(session, { file: file1Consumer1.path, line: 2, offset: 1, @@ -449,75 +474,52 @@ namespace ts.projectSystem { endOffset: 1, insertString: `export var T: number;` }); - session.executeCommand(changeModuleFile1ShapeRequest1); - session.executeCommand(changeFile1Consumer1ShapeRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); + + sendAffectedFileRequestAndCheckResult(session, + { projectFileName: configFile.path, file: moduleFile1.path }, + { projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }); }); it("should work fine for files with circular references", () => { - const file1: FileOrFolder = { - path: "/a/b/file1.ts", - content: ` - /// - export var t1 = 10;` - }; - const file2: FileOrFolder = { - path: "/a/b/file2.ts", - content: ` - /// - export var t2 = 10;` - }; + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const file1 = host.vfs.addFile("/a/b/file1.ts", `/// \nexport var t1 = 10;`); + const file2 = host.vfs.addFile("/a/b/file2.ts", `/// \nexport var t2 = 10;`); - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(file1.path, file1.content); - host.vfs.addFile(file2.path, file2.content); - host.vfs.addFile(configFile.path, configFile.content); - - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); + const session = createSession(host, createTestTypingsInstaller(host)); openFilesForSession([file1, file2], session); - const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); + + sendAffectedFileRequestAndCheckResult(session, + { file: file1.path }, + { projectFileName: configFile.path, files: [file1, file2] }); }); it("should return results for all projects if not specifying projectFileName", () => { - const file1: FileOrFolder = { path: "/a/b/file1.ts", content: "export var t = 10;" }; - const file2: FileOrFolder = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; - const file3: FileOrFolder = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; - const configFile1: FileOrFolder = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; - const configFile2: FileOrFolder = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(file1.path, file1.content); - host.vfs.addFile(file2.path, file2.content); - host.vfs.addFile(file3.path, file3.content); - host.vfs.addFile(configFile1.path, configFile1.content); - host.vfs.addFile(configFile2.path, configFile2.content); + const host = new mocks.MockServerHost(); + const file1 = host.vfs.addFile("/a/b/file1.ts", `export var t = 10;`); + const file2 = host.vfs.addFile("/a/b/file2.ts", `import {t} from "./file1"; var t2 = 11;`); + const file3 = host.vfs.addFile("/a/c/file2.ts", `import {t} from "../b/file1"; var t3 = 11;`); + const configFile1 = host.vfs.addFile("/a/b/tsconfig.json", `{ "compileOnSave": true }`); + const configFile2 = host.vfs.addFile("/a/c/tsconfig.json", `{ "compileOnSave": true }`); const session = createSession(host); openFilesForSession([file1, file2, file3], session); - const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ - { projectFileName: configFile1.path, files: [file1, file2] }, - { projectFileName: configFile2.path, files: [file1, file3] } - ]); + sendAffectedFileRequestAndCheckResult(session, + { file: file1.path }, + [ + { projectFileName: configFile1.path, files: [file1, file2] }, + { projectFileName: configFile2.path, files: [file1, file3] } + ]); }); it("should detect removed code file", () => { - const referenceFile1: FileOrFolder = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(moduleFile1.path, moduleFile1.content); - host.vfs.addFile(referenceFile1.path, referenceFile1.content); - host.vfs.addFile(configFile.path, configFile.content); + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); + const referenceFile1 = host.vfs.addFile("/a/b/referenceFile1.ts", `/// \nexport var x = Foo();`); const session = createSession(host); @@ -525,35 +527,27 @@ namespace ts.projectSystem { host.vfs.removeFile(moduleFile1.path); - const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - sendAffectedFileRequestAndCheckResult(session, request, [ - { projectFileName: configFile.path, files: [referenceFile1] } - ]); + sendAffectedFileRequestAndCheckResult(session, + { file: referenceFile1.path }, + { projectFileName: configFile.path, files: [referenceFile1] }); - const requestForMissingFile = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); - sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); + sendAffectedFileRequestAndCheckResult(session, + { file: moduleFile1.path }, + []); }); it("should detect non-existing code file", () => { - const referenceFile1: FileOrFolder = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(referenceFile1.path, referenceFile1.content); - host.vfs.addFile(configFile.path, configFile.content); + const host = new mocks.MockServerHost(); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); + const referenceFile1 = host.vfs.addFile("/a/b/referenceFile1.ts", `/// \nexport var x = Foo();`); const session = createSession(host); openFilesForSession([referenceFile1], session); - const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - sendAffectedFileRequestAndCheckResult(session, request, [ - { projectFileName: configFile.path, files: [referenceFile1] } - ]); + sendAffectedFileRequestAndCheckResult(session, + { file: referenceFile1.path }, + { projectFileName: configFile.path, files: [referenceFile1] }); }); }); }); @@ -563,63 +557,33 @@ namespace ts.projectSystem { test("\n"); test("\r\n"); - function test(newLine: string) { - const lines = ["var x = 1;", "var y = 2;"]; - const path = "/a/app"; - const f = { - path: path + ts.Extension.Ts, - content: lines.join(newLine) - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true), /*executingFilePath*/ undefined, newLine); - host.vfs.addFile(f.path, f.content); + function test(newLine: "\r\n" | "\n") { + const host = new mocks.MockServerHost({ newLine }); + const f = host.vfs.addFile("/a/app.ts", `var x = 1;${newLine}var y = 2;`); const session = createSession(host); - const openRequest: server.protocol.OpenRequest = { - seq: 1, - type: "request", - command: server.protocol.CommandTypes.Open, - arguments: { file: f.path } - }; - session.executeCommand(openRequest); - const emitFileRequest: server.protocol.CompileOnSaveEmitFileRequest = { - seq: 2, - type: "request", - command: server.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: f.path } - }; - session.executeCommand(emitFileRequest); - const emitOutput = host.readFile(path + ts.Extension.Js); + + sendOpenRequest(session, { file: f.path }, 1); + sendCompileOnSaveEmitFileRequest(session, { file: f.path }, 2); + + const emitOutput = host.readFile("/a/app.js"); assert.equal(emitOutput, f.content + newLine, "content of emit output should be identical with the input + newline"); } }); it("should emit specified file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export function Foo() { return 10; }` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `import {Foo} from "./f1"; let y = Foo();` - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true), /*executingFilePath*/ undefined, "\r\n"); - host.vfs.addFile(file1.path, file1.content); - host.vfs.addFile(file2.path, file2.content); - host.vfs.addFile(configFile.path, configFile.content); + const host = new mocks.MockServerHost({ newLine: "\r\n" }); + const file1 = host.vfs.addFile("/a/b/f1.ts", `export function Foo() { return 10; }`); + const file2 = host.vfs.addFile("/a/b/f2.ts", `import {Foo} from "./f1"; let y = Foo();`); + const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{}`); host.vfs.addFile(libFile.path, libFile.content); const typingsInstaller = createTestTypingsInstaller(host); const session = createSession(host, { typingsInstaller }); openFilesForSession([file1, file2], session); - const compileFileRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path }); - session.executeCommand(compileFileRequest); + + sendCompileOnSaveEmitFileRequest(session, { file: file1.path, projectFileName: configFile.path }); const expectedEmittedFileName = "/a/b/f1.js"; assert.isTrue(host.fileExists(expectedEmittedFileName)); @@ -627,25 +591,13 @@ namespace ts.projectSystem { }); it("shoud not emit js files in external projects", () => { - const file1 = { - path: "/a/b/file1.ts", - content: "consonle.log('file1');" - }; - // file2 has errors. The emitting should not be blocked. - const file2 = { - path: "/a/b/file2.js", - content: "console.log'file2');" - }; - const file3 = { - path: "/a/b/file3.js", - content: "console.log('file3');" - }; const externalProjectName = "/a/b/externalproject"; - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(file1.path, file1.content); - host.vfs.addFile(file2.path, file2.content); - host.vfs.addFile(file3.path, file3.content); + const host = new mocks.MockServerHost(); + const file1 = host.vfs.addFile("/a/b/file1.ts", `consonle.log('file1');`); + // file2 has errors. The emit should not be blocked. + const file2 = host.vfs.addFile("/a/b/file2.js", `console.log'file2');`); + const file3 = host.vfs.addFile("/a/b/file3.js", `console.log('file3');`); host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host); @@ -661,8 +613,7 @@ namespace ts.projectSystem { projectFileName: externalProjectName }); - const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); - session.executeCommand(emitRequest); + sendCompileOnSaveEmitFileRequest(session, { file: file1.path }); const expectedOutFileName = "/a/b/dist.js"; assert.isTrue(host.fileExists(expectedOutFileName)); @@ -673,15 +624,10 @@ namespace ts.projectSystem { }); it("should use project root as current directory so that compile on save results in correct file mapping", () => { - const inputFileName = "Foo.ts"; - const file1 = { - path: `/root/TypeScriptProject3/TypeScriptProject3/${inputFileName}`, - content: "consonle.log('file1');" - }; const externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; - const host = new mocks.MockServerHost(new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true)); - host.vfs.addFile(file1.path, file1.content); + const host = new mocks.MockServerHost(); + const file1 = host.vfs.addFile("/root/TypeScriptProject3/TypeScriptProject3/Foo.ts", `consonle.log('file1');`); host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host); @@ -698,8 +644,7 @@ namespace ts.projectSystem { projectFileName: externalProjectName }); - const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); - session.executeCommand(emitRequest); + sendCompileOnSaveEmitFileRequest(session, { file: file1.path }); // Verify js file const expectedOutFileName = "/root/TypeScriptProject3/TypeScriptProject3/" + outFileName; @@ -712,7 +657,7 @@ namespace ts.projectSystem { const expectedMapFileName = expectedOutFileName + ".map"; assert.isTrue(host.fileExists(expectedMapFileName)); const mapFileContent = host.readFile(expectedMapFileName); - verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); + verifyContentHasString(mapFileContent, `"sources":["Foo.ts"]`); function verifyContentHasString(content: string, str: string) { assert.isTrue(stringContains(content, str), `Expected "${content}" to have "${str}"`); diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index dae465a3ef3..7ea2af973d0 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -1,6 +1,7 @@ /// /// /// +/// namespace ts.projectSystem { describe("Project errors", () => { @@ -30,177 +31,127 @@ namespace ts.projectSystem { } it("external project - diagnostics for missing files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/applib.ts", - content: "" - }; - const host = createServerHost([file1, libFile]); + const host = new mocks.MockServerHost({ safeList: true, lib: true }); + host.vfs.addFile("/a/b/app.ts", ``); + + const projectFileName = "/a/b/test.csproj"; + const session = createSession(host); const projectService = session.getProjectService(); - const projectFileName = "/a/b/test.csproj"; - const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - }; + projectService.openExternalProject({ + projectFileName, + options: {}, + rootFiles: toExternalFiles(["/a/b/app.ts", "/a/b/applib.ts"]) + }); - { - projectService.openExternalProject({ - projectFileName, - options: {}, - rootFiles: toExternalFiles([file1.path, file2.path]) - }); + // only file1 exists - expect error + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags1 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName }, /*seq*/ 2); + checkDiagnosticsWithLinePos(diags1, ["File '/a/b/applib.ts' not found."]); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - // only file1 exists - expect error - checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); - } - host.reloadFS([file2, libFile]); - { - // only file2 exists - expect error - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); - } + host.vfs.removeFile("/a/b/app.ts"); + host.vfs.addFile("/a/b/applib.ts", ``); - host.reloadFS([file1, file2, libFile]); - { - // both files exist - expect no errors - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, []); - } + // only file2 exists - expect error + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags2 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName }, /*seq*/ 2); + checkDiagnosticsWithLinePos(diags2, ["File '/a/b/app.ts' not found."]); + + host.vfs.addFile("/a/b/app.ts", ``); + + // both files exist - expect no errors + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags3 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName }, /*seq*/ 2); + checkDiagnosticsWithLinePos(diags3, []); }); it("configured projects - diagnostics for missing files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/applib.ts", - content: "" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const host = createServerHost([file1, config, libFile]); + const host = new mocks.MockServerHost({ safeList: true, lib: true }); + host.vfs.addFile("/a/b/app.ts", ``); + host.vfs.addFile("/a/b/tsconfig.json", `{ "files": ["app.ts", "applib.ts"] }`); + const session = createSession(host); const projectService = session.getProjectService(); - openFilesForSession([file1], session); + + openFilesForSession(["/a/b/app.ts"], session); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); const project = configuredProjectAt(projectService, 0); - const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: project.getProjectName() } - }; - let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); + const diags1 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName: project.getProjectName() }, /*seq*/ 2); + checkDiagnosticsWithLinePos(diags1, ["File '/a/b/applib.ts' not found."]); - host.reloadFS([file1, file2, config, libFile]); + host.vfs.addFile("/a/b/applib.ts", ``); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, []); + const diags2 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName: project.getProjectName() }, /*seq*/ 2); + checkDiagnosticsWithLinePos(diags2, []); }); it("configured projects - diagnostics for corrupted config 1", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/lib.ts", - content: "" - }; - const correctConfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = createServerHost([file1, file2, corruptedConfig]); + const host = new mocks.MockServerHost({ safeList: true }); + host.vfs.addFile("/a/b/app.ts", ``); + host.vfs.addFile("/a/b/lib.ts", ``); + host.vfs.addFile("/a/b/tsconfig.json", ` "files": ["app.ts", "lib.ts"] }`); + const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, [ - "'{' expected." - ]); - assert.isNotNull(projectErrors[0].file); - assert.equal(projectErrors[0].file.fileName, corruptedConfig.path); - } + projectService.openClientFile("/a/b/app.ts"); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + + const configuredProject1 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json"); + assert.isTrue(configuredProject1 !== undefined, "should find configured project"); + checkProjectErrors(configuredProject1, []); + + const projectErrors1 = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors1, ["'{' expected."]); + assert.isNotNull(projectErrors1[0].file); + assert.equal(projectErrors1[0].file.fileName, "/a/b/tsconfig.json"); + // fix config and trigger watcher - host.reloadFS([file1, file2, correctConfig]); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } + host.vfs.writeFile("/a/b/tsconfig.json", `{ "files": ["app.ts", "lib.ts"] }`); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + + const configuredProject2 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json"); + assert.isTrue(configuredProject2 !== undefined, "should find configured project"); + checkProjectErrors(configuredProject2, []); + + const projectErrors2 = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors2, []); }); it("configured projects - diagnostics for corrupted config 2", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/lib.ts", - content: "" - }; - const correctConfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = createServerHost([file1, file2, correctConfig]); + const host = new mocks.MockServerHost({ safeList: true }); + host.vfs.addFile("/a/b/app.ts", ``); + host.vfs.addFile("/a/b/lib.ts", ``); + host.vfs.addFile("/a/b/tsconfig.json", `{ "files": ["app.ts", "lib.ts"] }`); + const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } + projectService.openClientFile("/a/b/app.ts"); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject1 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json"); + assert.isTrue(configuredProject1 !== undefined, "should find configured project"); + checkProjectErrors(configuredProject1, []); + + const projectErrors1 = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors1, []); + // break config and trigger watcher - host.reloadFS([file1, file2, corruptedConfig]); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, [ - "'{' expected." - ]); - assert.isNotNull(projectErrors[0].file); - assert.equal(projectErrors[0].file.fileName, corruptedConfig.path); - } + host.vfs.writeFile("/a/b/tsconfig.json", ` "files": ["app.ts", "lib.ts"] }`); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + + const configuredProject2 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json"); + assert.isTrue(configuredProject2 !== undefined, "should find configured project"); + checkProjectErrors(configuredProject2, []); + + const projectErrors2 = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors2, ["'{' expected."]); + assert.isNotNull(projectErrors2[0].file); + assert.equal(projectErrors2[0].file.fileName, "/a/b/tsconfig.json"); }); }); } diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 7e61f188b10..83bf2217db5 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -915,7 +915,7 @@ namespace ts { } function verifyProgram(vfs: vfs.VirtualFileSystem, rootFiles: string[], options: CompilerOptions, configFile: string) { - const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost(vfs)); + const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost({ vfs })); verifyProgramWithoutConfigFile(watchingSystemHost, rootFiles, options); verifyProgramWithConfigFile(watchingSystemHost, configFile); } @@ -997,7 +997,7 @@ namespace ts { `export default classD;`); const configFile = fs.addFile("/src/tsconfig.json", JSON.stringify({ compilerOptions, include: ["packages/**/ *.ts"] })); - const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost(fs)); + const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost({ vfs: fs })); verifyProgramWithConfigFile(watchingSystemHost, configFile.path); }); }); diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index e173df04abc..22ec81d583e 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1,14 +1,16 @@ /// /// /// +/// +/// namespace ts.tscWatch { + import theory = utils.theory; + import Spy = typemock.Spy; + import Arg = typemock.Arg; + import Times = typemock.Times; - import WatchedSystem = ts.TestFSWithWatch.TestServerHost; - type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; - import createWatchedSystem = ts.TestFSWithWatch.createWatchedSystem; import checkFileNames = ts.TestFSWithWatch.checkFileNames; - import libFile = ts.TestFSWithWatch.libFile; import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; import checkOutputContains = ts.TestFSWithWatch.checkOutputContains; @@ -41,46 +43,27 @@ namespace ts.tscWatch { return ts.createWatchModeWithoutConfigFile(fileNames, options, watchingSystemHost); } - function getEmittedLineForMultiFileOutput(file: FileOrFolder, host: ts.System) { - return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; + function formatOutputFile(path: string, host: ts.System) { + return `TSFILE: ${path}${host.newLine}`; } - function getEmittedLineForSingleFileOutput(filename: string, host: ts.System) { - return `TSFILE: ${filename}${host.newLine}`; - } - - interface FileOrFolderEmit extends FileOrFolder { - output?: string; - } - - function getFileOrFolderEmit(file: FileOrFolder, getOutput?: (file: FileOrFolder) => string): FileOrFolderEmit { - const result = file as FileOrFolderEmit; - if (getOutput) { - result.output = getOutput(file); - } - return result; - } - - function getEmittedLines(files: FileOrFolderEmit[]) { - const seen = createMap(); - const result: string[] = []; - for (const { output } of files) { - if (output && !seen.has(output)) { - seen.set(output, true); - result.push(output); + function getEmittedLines(files: ReadonlyArray, host: ts.System, getOutput: (file: string, host: ts.System) => string) { + let result: string[] | undefined; + if (files) { + const seen = createMap(); + result = []; + for (const file of files) { + const output = getOutput(file, host); + if (output && !seen.has(output)) { + seen.set(output, true); + result.push(output); + } } } return result; } - function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { - const expectedAffectedFiles = getEmittedLines(affectedFiles); - const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); - checkOutputContains(host, expectedAffectedFiles); - checkOutputDoesNotContain(host, expectedNonAffectedFiles); - } - - function checkOutputErrors(host: WatchedSystem, errors: ReadonlyArray, isInitial?: true, skipWaiting?: true) { + function checkOutputErrors(host: mocks.MockServerHost, errors: ReadonlyArray, isInitial?: true, skipWaiting?: true) { const outputs = host.getOutput(); const expectedOutputCount = (isInitial ? 0 : 1) + errors.length + (skipWaiting ? 0 : 1); assert.equal(outputs.length, expectedOutputCount, "Outputs = " + outputs.toString()); @@ -99,1832 +82,1297 @@ namespace ts.tscWatch { host.clearOutput(); } - function assertDiagnosticAt(host: WatchedSystem, outputAt: number, diagnostic: Diagnostic) { + function assertDiagnosticAt(host: mocks.MockServerHost, outputAt: number, diagnostic: Diagnostic) { const output = host.getOutput()[outputAt]; assert.equal(output, formatDiagnostic(diagnostic, host), "outputs[" + outputAt + "] is " + output); } - function assertWatchDiagnosticAt(host: WatchedSystem, outputAt: number, diagnosticMessage: DiagnosticMessage) { + function assertWatchDiagnosticAt(host: mocks.MockServerHost, outputAt: number, diagnosticMessage: DiagnosticMessage) { const output = host.getOutput()[outputAt]; assert.isTrue(endsWith(output, getWatchDiagnosticWithoutDate(host, diagnosticMessage)), "outputs[" + outputAt + "] is " + output); } - function getWatchDiagnosticWithoutDate(host: WatchedSystem, diagnosticMessage: DiagnosticMessage) { + function getWatchDiagnosticWithoutDate(host: mocks.MockServerHost, diagnosticMessage: DiagnosticMessage) { return ` - ${flattenDiagnosticMessageText(getLocaleSpecificMessage(diagnosticMessage), host.newLine)}${host.newLine + host.newLine + host.newLine}`; } - function getDiagnosticOfFileFrom(file: SourceFile, text: string, start: number, length: number, message: DiagnosticMessage): Diagnostic { + function getFile(program: Program, filePath: string) { + return program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())); + } + + function getConfigFile(program: Program) { + return program.getCompilerOptions().configFile; + } + + function createDiagnostic(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage, ...args: string[]): Diagnostic { + let text = getLocaleSpecificMessage(message); + if (args.length > 0) { + text = formatStringFromArgs(text, args); + } return { file, start, length, - messageText: text, category: message.category, code: message.code, }; } - function getDiagnosticWithoutFile(message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { - let text = getLocaleSpecificMessage(message); - - if (arguments.length > 1) { - text = formatStringFromArgs(text, arguments, 1); - } - - return getDiagnosticOfFileFrom(/*file*/ undefined, text, /*start*/ undefined, /*length*/ undefined, message); + function createCompilerDiagnostic(message: DiagnosticMessage, ...args: string[]): Diagnostic { + return createDiagnostic(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); } - function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { - let text = getLocaleSpecificMessage(message); - - if (arguments.length > 4) { - text = formatStringFromArgs(text, arguments, 4); - } - - return getDiagnosticOfFileFrom(file, text, start, length, message); + function createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ...args: string[]): Diagnostic { + return createDiagnostic(file, start, length, message, ...args); } - function getUnknownCompilerOption(program: Program, configFile: FileOrFolder, option: string) { + function createUnknownCompilerOptionDiagnostic(program: Program, content: string, option: string) { const quotedOption = `"${option}"`; - return getDiagnosticOfFile(program.getCompilerOptions().configFile, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); + return createFileDiagnostic(getConfigFile(program), content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); } - function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage, ..._args: (string | number)[]): Diagnostic { - let text = getLocaleSpecificMessage(message); - - if (arguments.length > 5) { - text = formatStringFromArgs(text, arguments, 5); - } - - return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())), - text, start, length, message); + function createExclusiveCompilerOptionDiagnostic(program: Program, content: string, option1: string, option2: string, checkFirst: boolean) { + const quotedOption1 = `"${checkFirst ? option1 : option2 }"`; + return createFileDiagnostic(getConfigFile(program), content.indexOf(quotedOption1), quotedOption1.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, option1, option2); } - function getDiagnosticModuleNotFoundOfFile(program: Program, file: FileOrFolder, moduleName: string) { + function createCannotFindModuleDiagnostic(program: Program, path: string, content: string, moduleName: string) { const quotedModuleName = `"${moduleName}"`; - return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); + return createFileDiagnostic(getFile(program, path), content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); } - describe("tsc-watch program updates", () => { - const commonFile1: FileOrFolder = { - path: "/a/b/commonFile1.ts", - content: "let x = 1" - }; - const commonFile2: FileOrFolder = { - path: "/a/b/commonFile2.ts", - content: "let y = 1" - }; + function createFileIsNotAModuleDiagnostic(program: Program, path: string, content: string, moduleName: string, modulePath: string) { + const quotedModuleName = `"${moduleName}"`; + return createFileDiagnostic(getFile(program, path), content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.File_0_is_not_a_module, modulePath); + } - it("create watch without config file", () => { - const appFile: FileOrFolder = { - path: "/a/b/c/app.ts", - content: ` - import {f} from "./module" - console.log(f) - ` - }; + function createFileNotFoundDiagnostic(program: Program, path: string, content: string, fragment: string, file: string) { + return createFileDiagnostic(getFile(program, path), content.indexOf(fragment), fragment.length, Diagnostics.File_0_not_found, file); + } - const moduleFile: FileOrFolder = { - path: "/a/b/c/module.d.ts", - content: `export let x: number` - }; - const host = createWatchedSystem([appFile, moduleFile, libFile]); - const watch = createWatchModeWithoutConfigFile([appFile.path], host); + function createCannotFindNameDiagnostic(program: Program, path: string, content: string, name: string) { + return createFileDiagnostic(getFile(program, path), content.indexOf(name), name.length, Diagnostics.Cannot_find_name_0, name); + } - checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); + describe("tsc-watch", () => { - // TODO: Should we watch creation of config files in the root file's file hierarchy? + describe("program updates", () => { + it("create watch without config file", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/c/app.ts", `import {f} from "./module"\nconsole.log(f)`); + host.vfs.addFile("/a/b/c/module.d.ts", `export let x: number`); - // const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; - // const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - // checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + const watch = createWatchModeWithoutConfigFile(["/a/b/c/app.ts"], host); + checkProgramActualFiles(watch(), ["/a/b/c/app.ts", mocks.MockServerHost.libPath, "/a/b/c/module.d.ts"]); + + // TODO: Should we watch creation of config files in the root file's file hierarchy? + + // const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + // const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + // checkWatchedFiles(host, configFiles.concat(mocks.MockServerHost.libPath, "/a/b/c/module.d.ts")); + }); + + it("can handle tsconfig file name with difference casing", () => { + const host = new mocks.MockServerHost({ vfs: { useCaseSensitiveFileNames: false } }); + host.vfs.addFile("/a/b/app.ts", `let x = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "include": ["app.ts"] }`); + + const watch = createWatchModeWithConfigFile("/A/B/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/A/B/app.ts"]); + }); + + it("create configured project without file list", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "exclude": ["e"] }`); + host.vfs.addFile("/a/b/c/f1.ts", `let x = 1`); + host.vfs.addFile("/a/b/d/f2.ts", `let y = 1`); + host.vfs.addFile("/a/b/e/f3.ts", `let z = 1`); + + const watchingSystemHost = createWatchingSystemHost(host); + const configFileResult = parseConfigFile("/a/b/tsconfig.json", watchingSystemHost); + assert.equal(configFileResult.errors.length, 0, `expect no errors in config file, got ${JSON.stringify(configFileResult.errors)}`); + + const watch = ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); + + checkProgramActualFiles(watch(), ["/a/b/c/f1.ts", mocks.MockServerHost.libPath, "/a/b/d/f2.ts"]); + checkProgramRootFiles(watch(), ["/a/b/c/f1.ts", "/a/b/d/f2.ts"]); + checkWatchedFiles(host, ["/a/b/tsconfig.json", "/a/b/c/f1.ts", "/a/b/d/f2.ts", mocks.MockServerHost.libPath]); + checkWatchedDirectories(host, ["/a/b", "/a/b/node_modules/@types"], /*recursive*/ true); + }); + + // TODO: if watching for config file creation + // it("add and then remove a config file in a folder with loose files", () => { + // }); + + it("add new files to a configured program without file list", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/commonFile1.ts", `let x = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{}`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkWatchedDirectories(host, ["/a/b", "/a/b/node_modules/@types"], /*recursive*/ true); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); + + // add a new ts file + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); + }); + + it("should ignore non-existing files specified in the config file", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/commonFile1.ts", `let x = 1`); + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "files": ["commonFile1.ts", "commonFile3.ts"] }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile3.ts"]); + checkProgramActualFiles(watch(), ["/a/b/commonFile1.ts"]); + }); + + it("handle recreated files correctly", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/commonFile1.ts", `let x = 1`); + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{}`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); + + // delete commonFile2 + host.vfs.removeFile("/a/b/commonFile2.ts"); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); + + // re-add commonFile2 + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); + }); + + it("handles the missing files - that were added to program because they were added with /// { + const file1Content = `/// \nlet x = y`; + + const host = new mocks.MockServerHost({ lib: true, vfs: { useCaseSensitiveFileNames: false } }); + host.vfs.addFile("/a/b/commonFile1.ts", file1Content); + + const watch = createWatchModeWithoutConfigFile(["/a/b/commonFile1.ts"], host); + + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); + checkProgramActualFiles(watch(), ["/a/b/commonFile1.ts", mocks.MockServerHost.libPath]); + checkOutputErrors(host, [ + createFileNotFoundDiagnostic(watch(), "/a/b/commonFile1.ts", file1Content, "commonFile2.ts", "/a/b/commonFile2.ts"), + createCannotFindNameDiagnostic(watch(), "/a/b/commonFile1.ts", file1Content, "y"), + ], /*isInitial*/ true); + + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); + checkProgramActualFiles(watch(), ["/a/b/commonFile1.ts", mocks.MockServerHost.libPath, "/a/b/commonFile2.ts"]); + checkOutputErrors(host, emptyArray); + }); + + it("should reflect change in config file", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/commonFile1.ts", `let x = 1`); + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "files": ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"] }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); + + host.vfs.writeFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "files": ["/a/b/commonFile1.ts"] }`); + host.checkTimeoutQueueLengthAndRun(1); // reload the configured project + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); + }); + + it("files explicitly excluded in config file", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/commonFile1.ts", `let x = 1`); + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + host.vfs.addFile("/a/c/excludedFile1.ts", `let t = 1;`); + host.vfs.addFile("/a/tsconfig.json", `{ "compilerOptions": {}, "exclude": ["/a/c"] }`); + + const watch = createWatchModeWithConfigFile("/a/tsconfig.json", host); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); + }); + + it("should properly handle module resolution changes in config file", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/file1.ts", `import { T } from "module1";`); + host.vfs.addFile("/a/b/node_modules/module1.ts", `export interface T {}`); + host.vfs.addFile("/a/module1.ts", `export interface T {}`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": { "moduleResolution": "node" }, "files": ["/a/b/file1.ts"] }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramRootFiles(watch(), ["/a/b/file1.ts"]); + checkProgramActualFiles(watch(), ["/a/b/file1.ts", "/a/b/node_modules/module1.ts"]); + + host.vfs.writeFile("/a/b/tsconfig.json", `{ "compilerOptions": { "moduleResolution": "classic" }, "files": ["/a/b/file1.ts"] }`); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/file1.ts"]); + checkProgramActualFiles(watch(), ["/a/b/file1.ts", "/a/module1.ts"]); + }); + + it("should tolerate config file errors and still try to build a project", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/commonFile1.ts", `let x = 1`); + host.vfs.addFile("/a/b/commonFile2.ts", `let y = 1`); + host.vfs.addFile("/a/b/tsconfig.json", + `{\n` + + ` "compilerOptions": {\n` + + ` "target": "es6",\n` + + ` "allowAnything": true\n` + + ` },\n` + + ` "someOtherProperty": {}\n` + + `}`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); + }); + + it("changes in files are reflected in project structure", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", `export * from "./f2"`); + host.vfs.addFile("/a/b/f2.ts", `export let x = 1`); + host.vfs.addFile("/a/c/f3.ts", `export let y = 1;`); + + const watch = createWatchModeWithoutConfigFile(["/a/b/f1.ts"], host); + checkProgramRootFiles(watch(), ["/a/b/f1.ts"]); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + + host.vfs.writeFile("/a/b/f2.ts", `export * from "../c/f3"`); // now inferred project should inclule file3 + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/f1.ts"]); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts", "/a/c/f3.ts"]); + }); + + it("deleted files affect project structure", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", `export * from "./f2"`); + host.vfs.addFile("/a/b/f2.ts", `export * from "../c/f3"`); + host.vfs.addFile("/a/c/f3.ts", `export let y = 1;`); + + const watch = createWatchModeWithoutConfigFile(["/a/b/f1.ts"], host); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts", "/a/c/f3.ts"]); + + host.vfs.removeFile("/a/b/f2.ts"); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramActualFiles(watch(), ["/a/b/f1.ts"]); + }); + + it("deleted files affect project structure - 2", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", `export * from "./f2"`); + host.vfs.addFile("/a/b/f2.ts", `export * from "../c/f3"`); + host.vfs.addFile("/a/c/f3.ts", `export let y = 1;`); + + const watch = createWatchModeWithoutConfigFile(["/a/b/f1.ts", "/a/c/f3.ts"], host); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts", "/a/c/f3.ts"]); + + host.vfs.removeFile("/a/b/f2.ts"); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/c/f3.ts"]); + }); + + it("config file includes the file", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", `export let x = 5`); + host.vfs.addFile("/a/c/f2.ts", `import {x} from "../b/f1"`); + host.vfs.addFile("/a/c/f3.ts", `export let y = 1`); + host.vfs.addFile("/a/c/tsconfig.json", `{ "compilerOptions": {}, "files": ["f2.ts", "f3.ts"] }`); + + const watch = createWatchModeWithConfigFile("/a/c/tsconfig.json", host); + checkProgramRootFiles(watch(), ["/a/c/f2.ts", "/a/c/f3.ts"]); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/c/f2.ts", "/a/c/f3.ts"]); + }); + + it("correctly migrate files between projects", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", + `export * from "../c/f2";\n` + + `export * from "../d/f3";`); + host.vfs.addFile("/a/c/f2.ts", `export let x = 1;`); + host.vfs.addFile("/a/d/f3.ts", `export let y = 1;`); + + const watch1 = createWatchModeWithoutConfigFile(["/a/c/f2.ts", "/a/d/f3.ts"], host); + checkProgramActualFiles(watch1(), ["/a/c/f2.ts", "/a/d/f3.ts"]); + + const watch2 = createWatchModeWithoutConfigFile(["/a/b/f1.ts"], host); + checkProgramActualFiles(watch2(), ["/a/b/f1.ts", "/a/c/f2.ts", "/a/d/f3.ts"]); + + // Previous program shouldnt be updated + checkProgramActualFiles(watch1(), ["/a/c/f2.ts", "/a/d/f3.ts"]); + host.checkTimeoutQueueLength(0); + }); + + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", `let x = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {} }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/a/b/f1.ts"]); + + host.vfs.addFile("/a/b/f2.ts", `let y = 1`); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + checkProgramRootFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + }); + + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", `let x = 1`); + host.vfs.addFile("/a/b/f2.ts", `let y = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "files": ["f1.ts"] }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/a/b/f1.ts"]); + + host.vfs.writeFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "files": ["f1.ts", "f2.ts"] }`); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + }); + + it("can update configured project when set of root files was not changed", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/f1.ts", `let x = 1`); + host.vfs.addFile("/a/b/f2.ts", `let y = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "files": ["f1.ts", "f2.ts"] }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + + host.vfs.writeFile("/a/b/tsconfig.json", `{ "compilerOptions": { "outFile": "out.js" }, "files": ["f1.ts", "f2.ts"] }`); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); + }); + + it("config file is deleted", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/f1.ts", `let x = 1`); + host.vfs.addFile("/a/b/f2.ts", `let y = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {} }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + + checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts", mocks.MockServerHost.libPath]); + checkOutputErrors(host, emptyArray, /*isInitial*/ true); + + host.vfs.removeFile("/a/b/tsconfig.json"); + host.checkTimeoutQueueLengthAndRun(1); + + assert.equal(host.exitCode, ExitStatus.DiagnosticsPresent_OutputsSkipped); + checkOutputErrors(host, [ + createCompilerDiagnostic(Diagnostics.File_0_not_found, "/a/b/tsconfig.json") + ], /*isInitial*/ undefined, /*skipWaiting*/ true); + }); + + it("Proper errors: document is not contained in project", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/b/app.ts", ``); + host.vfs.addFile("/a/b/tsconfig.json", `{`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/a/b/app.ts"]); + }); + + it("correctly handles changes in lib section of config file", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/.ts/lib.es5.d.ts", `declare const eval: any`); + host.vfs.addFile("/.ts/lib.es2015.promise.d.ts", `declare class Promise {}`); + host.vfs.addFile("/src/app.ts", `var x: Promise;`); + host.vfs.addFile("/src/tsconfig.json", `{ "compilerOptions": { "lib": ["es5"] } }`); + + const watch = createWatchModeWithConfigFile("/src/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/.ts/lib.es5.d.ts", "/src/app.ts"]); + + host.vfs.writeFile("/src/tsconfig.json", `{ "compilerOptions": { "lib": ["es5", "es2015.promise"] } }`); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramActualFiles(watch(), ["/.ts/lib.es5.d.ts", "/.ts/lib.es2015.promise.d.ts", "/src/app.ts"]); + }); + + it("should handle non-existing directories in config file", () => { + const host = new mocks.MockServerHost(); + host.vfs.addFile("/a/src/app.ts", `let x = 1;`); + host.vfs.addFile("/a/tsconfig.json", `{ "compilerOptions": {}, "include": ["src/**/*", "notexistingfolder/*"] }`); + + const watch = createWatchModeWithConfigFile("/a/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/a/src/app.ts"]); + }); + + it("rename a module file and rename back should restore the states for inferred projects", () => { + const file1Content = `import * as T from "./moduleFile"; T.bar();`; + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/file1.ts", file1Content); + host.vfs.addFile("/a/b/moduleFile.ts", `export function bar() { };`); + + const watch = createWatchModeWithoutConfigFile(["/a/b/file1.ts"], host); + checkOutputErrors(host, emptyArray, /*isInitial*/ true); + + host.vfs.removeFile("/a/b/moduleFile.js"); + host.vfs.rename("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, [ + createCannotFindModuleDiagnostic(watch(), "/a/b/file1.ts", file1Content, "./moduleFile") + ]); + + host.vfs.removeFile("/a/b/moduleFile1.js"); + host.vfs.rename("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray); + }); + + it("rename a module file and rename back should restore the states for configured projects", () => { + const file1Content = `import * as T from "./moduleFile"; T.bar();`; + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/file1.ts", file1Content); + host.vfs.addFile("/a/b/moduleFile.ts", `export function bar() { };`); + host.vfs.addFile("/a/b/tsconfig.json", `{}`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkOutputErrors(host, emptyArray, /*isInitial*/ true); + + host.vfs.removeFile("/a/b/moduleFile.js"); + host.vfs.rename("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, [ + createCannotFindModuleDiagnostic(watch(), "/a/b/file1.ts", file1Content, "./moduleFile") + ]); + + host.vfs.removeFile("/a/b/moduleFile1.js"); + host.vfs.rename("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray); + }); + + it("types should load from config file path if config exists", () => { + const host = new mocks.MockServerHost({ vfs: { currentDirectory: "/a/c" }}); + host.vfs.addDirectory("/a/c"); + host.vfs.addFile("/a/b/app.ts", `let x = 1`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": { "types": ["node"], "typeRoots": [] } }`); + host.vfs.addFile("/a/b/node_modules/@types/node/index.d.ts", `declare var process: any`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramActualFiles(watch(), ["/a/b/app.ts", "/a/b/node_modules/@types/node/index.d.ts"]); + }); + + it("add the missing module file for inferred project: should remove the `module not found` error", () => { + const file1Content = `import * as T from "./moduleFile"; T.bar();`; + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/file1.ts", file1Content); + + const watch = createWatchModeWithoutConfigFile(["/a/b/file1.ts"], host); + + checkOutputErrors(host, [ + createCannotFindModuleDiagnostic(watch(), "/a/b/file1.ts", file1Content, "./moduleFile") + ], /*isInitial*/ true); + + host.vfs.addFile("/a/b/moduleFile.ts", `export function bar() { };`); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray); + }); + + it("Configure file diagnostics events are generated when the config file has errors", () => { + const configFileContent = `{ "compilerOptions": { "foo": "bar", "allowJS": true } }`; + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/app.ts", `let x = 10`); + host.vfs.addFile("/a/b/tsconfig.json", configFileContent); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkOutputErrors(host, [ + createUnknownCompilerOptionDiagnostic(watch(), configFileContent, "foo"), + createUnknownCompilerOptionDiagnostic(watch(), configFileContent, "allowJS") + ], /*isInitial*/ true); + }); + + it("If config file doesnt have errors, they are not reported", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/app.ts", `let x = 10`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {} }`); + + createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkOutputErrors(host, emptyArray, /*isInitial*/ true); + }); + + it("Reports errors when the config file changes", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/app.ts", `let x = 10`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {} }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkOutputErrors(host, emptyArray, /*isInitial*/ true); + + const configFileBadContent = `{ "compilerOptions": { "haha": 123 } }`; + host.vfs.writeFile("/a/b/tsconfig.json", configFileBadContent); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, [ + createUnknownCompilerOptionDiagnostic(watch(), configFileBadContent, "haha") + ]); + + host.vfs.writeFile("/a/b/tsconfig.json", `{ "compilerOptions": {} }`); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray); + }); + + it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/file1.ts", `let t = 10;`); + host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": {}, "include": ["app/*", "test/**/*", "something"] }`); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkProgramActualFiles(watch(), [mocks.MockServerHost.libPath]); + }); + + it("non-existing directories listed in config file input array should be able to handle @types if input file list is empty", () => { + const host = new mocks.MockServerHost({ vfs: { currentDirectory: "/a/" } }); + host.vfs.addFile("/a/app.ts", `let x = 1`); + host.vfs.addFile("/a/tsconfig.json", `{ "compilerOptions": {}, "files": [] }`); + host.vfs.addFile("/a/node_modules/@types/typings/index.d.ts", `export * from "./lib"`); + host.vfs.addFile("/a/node_modules/@types/typings/lib.d.ts", `export const x: number`); + + const watch = createWatchModeWithConfigFile("/a/tsconfig.json", host); + + checkProgramActualFiles(watch(), ["/a/node_modules/@types/typings/index.d.ts", "/a/node_modules/@types/typings/lib.d.ts"]); + }); + + it("should support files without extensions", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/compile", `let x = 1`); + + const watch = createWatchModeWithoutConfigFile(["/a/compile"], host, { allowNonTsExtensions: true }); + checkProgramActualFiles(watch(), ["/a/compile", mocks.MockServerHost.libPath]); + }); + + it("Options Diagnostic locations reported correctly with changes in configFile contents when options change", () => { + const configFileContentComment = + ` // comment\n` + + ` // More comment\n`; + const configFileContentWithComment = + `{\n` + + configFileContentComment + + ` "compilerOptions": {\n` + + ` "allowJs": true,\n` + + ` "declaration": true\n` + + ` }\n` + + `}`; + const configFileContentWithoutComment = + `{\n` + + ` "compilerOptions": {\n` + + ` "allowJs": true,\n` + + ` "declaration": true\n` + + ` }\n` + + `}`; + + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/b/app.ts", `let x = 10`); + host.vfs.addFile("/a/b/tsconfig.json", configFileContentWithComment); + + const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + const initialErrors = [ + createExclusiveCompilerOptionDiagnostic(watch(), configFileContentWithComment, "allowJs", "declaration", /*checkFirst*/ true), + createExclusiveCompilerOptionDiagnostic(watch(), configFileContentWithComment, "allowJs", "declaration", /*checkFirst*/ false) + ]; + checkOutputErrors(host, initialErrors, /*isInitial*/ true); + + host.vfs.writeFile("/a/b/tsconfig.json", configFileContentWithoutComment); + host.runQueuedTimeoutCallbacks(); + const nowErrors = [ + createExclusiveCompilerOptionDiagnostic(watch(), configFileContentWithoutComment, "allowJs", "declaration", /*checkFirst*/ true), + createExclusiveCompilerOptionDiagnostic(watch(), configFileContentWithoutComment, "allowJs", "declaration", /*checkFirst*/ false) + ]; + checkOutputErrors(host, nowErrors); + + assert.equal(nowErrors[0].start, initialErrors[0].start - configFileContentComment.length); + assert.equal(nowErrors[1].start, initialErrors[1].start - configFileContentComment.length); + }); }); - it("can handle tsconfig file name with difference casing", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - include: ["app.ts"] - }) - }; - - const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); - const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); - const watch = createWatchModeWithConfigFile(upperCaseConfigFilePath, host); - checkProgramActualFiles(watch(), [combinePaths(getDirectoryPath(upperCaseConfigFilePath), getBaseFileName(f1.path))]); - }); - - it("create configured project without file list", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: ` - { - "compilerOptions": {}, - "exclude": [ - "e" - ] - }` - }; - const file1: FileOrFolder = { - path: "/a/b/c/f1.ts", - content: "let x = 1" - }; - const file2: FileOrFolder = { - path: "/a/b/d/f2.ts", - content: "let y = 1" - }; - const file3: FileOrFolder = { - path: "/a/b/e/f3.ts", - content: "let z = 1" - }; - - const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); - const watchingSystemHost = createWatchingSystemHost(host); - const configFileResult = parseConfigFile(configFile.path, watchingSystemHost); - assert.equal(configFileResult.errors.length, 0, `expect no errors in config file, got ${JSON.stringify(configFileResult.errors)}`); - - const watch = ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); - - checkProgramActualFiles(watch(), [file1.path, libFile.path, file2.path]); - checkProgramRootFiles(watch(), [file1.path, file2.path]); - checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); - const configDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); - }); - - // TODO: if watching for config file creation - // it("add and then remove a config file in a folder with loose files", () => { - // }); - - it("add new files to a configured program without file list", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createWatchedSystem([commonFile1, libFile, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - const configDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); - - checkProgramRootFiles(watch(), [commonFile1.path]); - - // add a new ts file - host.reloadFS([commonFile1, commonFile2, libFile, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("should ignore non-existing files specified in the config file", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "files": [ - "commonFile1.ts", - "commonFile3.ts" - ] - }` - }; - const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - - const commonFile3 = "/a/b/commonFile3.ts"; - checkProgramRootFiles(watch(), [commonFile1.path, commonFile3]); - checkProgramActualFiles(watch(), [commonFile1.path]); - }); - - it("handle recreated files correctly", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - - // delete commonFile2 - host.reloadFS([commonFile1, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [commonFile1.path]); - - // re-add commonFile2 - host.reloadFS([commonFile1, commonFile2, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("handles the missing files - that were added to program because they were added with /// { - const commonFile2Name = "commonFile2.ts"; - const file1: FileOrFolder = { - path: "/a/b/commonFile1.ts", - content: `/// - let x = y` - }; - const host = createWatchedSystem([file1, libFile]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); - - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, libFile.path]); - checkOutputErrors(host, [ - getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf(commonFile2Name), commonFile2Name.length, Diagnostics.File_0_not_found, commonFile2.path), - getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf("y"), 1, Diagnostics.Cannot_find_name_0, "y") - ], /*isInitial*/ true); - - host.reloadFS([file1, commonFile2, libFile]); - host.runQueuedTimeoutCallbacks(); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); - checkOutputErrors(host, emptyArray); - }); - - it("should reflect change in config file", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}", "${commonFile2.path}"] - }` - }; - const files = [commonFile1, commonFile2, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); - - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - configFile.content = `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}"] - }`; - - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); // reload the configured project - checkProgramRootFiles(watch(), [commonFile1.path]); - }); - - it("files explicitly excluded in config file", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "exclude": ["/a/c"] - }` - }; - const excludedFile1: FileOrFolder = { - path: "/a/c/excluedFile1.ts", - content: `let t = 1;` - }; - - const host = createWatchedSystem([commonFile1, commonFile2, excludedFile1, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("should properly handle module resolution changes in config file", () => { - const file1: FileOrFolder = { - path: "/a/b/file1.ts", - content: `import { T } from "module1";` - }; - const nodeModuleFile: FileOrFolder = { - path: "/a/b/node_modules/module1.ts", - content: `export interface T {}` - }; - const classicModuleFile: FileOrFolder = { - path: "/a/module1.ts", - content: `export interface T {}` - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "moduleResolution": "node" - }, - "files": ["${file1.path}"] - }` - }; - const files = [file1, nodeModuleFile, classicModuleFile, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, nodeModuleFile.path]); - - configFile.content = `{ - "compilerOptions": { - "moduleResolution": "classic" - }, - "files": ["${file1.path}"] - }`; - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, classicModuleFile.path]); - }); - - it("should tolerate config file errors and still try to build a project", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6", - "allowAnything": true - }, - "someOtherProperty": {} - }` - }; - const host = createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("changes in files are reflected in project structure", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export let x = 1` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - - const modifiedFile2 = { - path: file2.path, - content: `export * from "../c/f3"` // now inferred project should inclule file3 - }; - - host.reloadFS([file1, modifiedFile2, file3]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, modifiedFile2.path, file3.path]); - }); - - it("deleted files affect project structure", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); - checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); - - host.reloadFS([file1, file3]); - host.checkTimeoutQueueLengthAndRun(1); - - checkProgramActualFiles(watch(), [file1.path]); - }); - - it("deleted files affect project structure - 2", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path, file3.path], host); - checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); - - host.reloadFS([file1, file3]); - host.checkTimeoutQueueLengthAndRun(1); - - checkProgramActualFiles(watch(), [file1.path, file3.path]); - }); - - it("config file includes the file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "export let x = 5" - }; - const file2 = { - path: "/a/c/f2.ts", - content: `import {x} from "../b/f1"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: "export let y = 1" - }; - const configFile = { - path: "/a/c/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) - }; - - const host = createWatchedSystem([file1, file2, file3, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - - checkProgramRootFiles(watch(), [file2.path, file3.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); - }); - - it("correctly migrate files between projects", () => { - const file1 = { - path: "/a/b/f1.ts", - content: ` - export * from "../c/f2"; - export * from "../d/f3";` - }; - const file2 = { - path: "/a/c/f2.ts", - content: "export let x = 1;" - }; - const file3 = { - path: "/a/d/f3.ts", - content: "export let y = 1;" - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file2.path, file3.path], host); - checkProgramActualFiles(watch(), [file2.path, file3.path]); - - const watch2 = createWatchModeWithoutConfigFile([file1.path], host); - checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); - - // Previous program shouldnt be updated - checkProgramActualFiles(watch(), [file2.path, file3.path]); - host.checkTimeoutQueueLength(0); - }); - - it("can correctly update configured project when set of root files has changed (new file on disk)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - - const host = createWatchedSystem([file1, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), [file1.path]); - - host.reloadFS([file1, file2, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - - checkProgramActualFiles(watch(), [file1.path, file2.path]); - checkProgramRootFiles(watch(), [file1.path, file2.path]); - }); - - it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) - }; - - const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - - checkProgramActualFiles(watch(), [file1.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path, file2.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - }); - - it("can update configured project when set of root files was not changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path, file2.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - }); - - it("config file is deleted", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1;" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 2;" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const host = createWatchedSystem([file1, file2, libFile, config]); - const watch = createWatchModeWithConfigFile(config.path, host); - - checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); - checkOutputErrors(host, emptyArray, /*isInitial*/ true); - - host.reloadFS([file1, file2, libFile]); - host.checkTimeoutQueueLengthAndRun(1); - - assert.equal(host.exitCode, ExitStatus.DiagnosticsPresent_OutputsSkipped); - checkOutputErrors(host, [ - getDiagnosticWithoutFile(Diagnostics.File_0_not_found, config.path) - ], /*isInitial*/ undefined, /*skipWaiting*/ true); - }); - - it("Proper errors: document is not contained in project", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const corruptedConfig = { - path: "/a/b/tsconfig.json", - content: "{" - }; - const host = createWatchedSystem([file1, corruptedConfig]); - const watch = createWatchModeWithConfigFile(corruptedConfig.path, host); - - checkProgramActualFiles(watch(), [file1.path]); - }); - - it("correctly handles changes in lib section of config file", () => { - const libES5 = { - path: "/compiler/lib.es5.d.ts", - content: "declare const eval: any" - }; - const libES2015Promise = { - path: "/compiler/lib.es2015.promise.d.ts", - content: "declare class Promise {}" - }; - const app = { - path: "/src/app.ts", - content: "var x: Promise;" - }; - const config1 = { - path: "/src/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5" - ] - } - }) - }; - const config2 = { - path: config1.path, - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5", - "es2015.promise" - ] - } - }) - }; - const host = createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - const watch = createWatchModeWithConfigFile(config1.path, host); - - checkProgramActualFiles(watch(), [libES5.path, app.path]); - - host.reloadFS([libES5, libES2015Promise, app, config2]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramActualFiles(watch(), [libES5.path, libES2015Promise.path, app.path]); - }); - - it("should handle non-existing directories in config file", () => { - const f = { - path: "/a/src/app.ts", - content: "let x = 1;" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {}, - include: [ - "src/**/*", - "notexistingfolder/*" - ] - }) - }; - const host = createWatchedSystem([f, config]); - const watch = createWatchModeWithConfigFile(config.path, host); - checkProgramActualFiles(watch(), [f.path]); - }); - - it("rename a module file and rename back should restore the states for inferred projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - const host = createWatchedSystem([moduleFile, file1, libFile]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); - checkOutputErrors(host, emptyArray, /*isInitial*/ true); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ]); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray); - }); - - it("rename a module file and rename back should restore the states for configured projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*isInitial*/ true); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ]); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray); - }); - - it("types should load from config file path if config exists", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) - }; - const node = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: "declare var process: any" - }; - const cwd = { - path: "/a/c" - }; - const host = createWatchedSystem([f1, config, node, cwd], { currentDirectory: cwd.path }); - const watch = createWatchModeWithConfigFile(config.path, host); - - checkProgramActualFiles(watch(), [f1.path, node.path]); - }); - - it("add the missing module file for inferred project: should remove the `module not found` error", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - const host = createWatchedSystem([file1, libFile]); - const watch = createWatchModeWithoutConfigFile([file1.path], host); - - checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ], /*isInitial*/ true); - - host.reloadFS([file1, moduleFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray); - }); - - it("Configure file diagnostics events are generated when the config file has errors", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "foo": "bar", - "allowJS": true - } - }` - }; - - const host = createWatchedSystem([file, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, [ - getUnknownCompilerOption(watch(), configFile, "foo"), - getUnknownCompilerOption(watch(), configFile, "allowJS") - ], /*isInitial*/ true); - }); - - it("If config file doesnt have errors, they are not reported", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {} - }` - }; - - const host = createWatchedSystem([file, configFile, libFile]); - createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*isInitial*/ true); - }); - - it("Reports errors when the config file changes", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {} - }` - }; - - const host = createWatchedSystem([file, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*isInitial*/ true); - - configFile.content = `{ - "compilerOptions": { - "haha": 123 - } - }`; - host.reloadFS([file, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ - getUnknownCompilerOption(watch(), configFile, "haha") - ]); - - configFile.content = `{ - "compilerOptions": {} - }`; - host.reloadFS([file, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray); - }); - - it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "include": ["app/*", "test/**/*", "something"] - }` - }; - const file1 = { - path: "/a/b/file1.ts", - content: "let t = 10;" - }; - - const host = createWatchedSystem([file1, configFile, libFile]); - const watch = createWatchModeWithConfigFile(configFile.path, host); - - checkProgramActualFiles(watch(), [libFile.path]); - }); - - it("non-existing directories listed in config file input array should be able to handle @types if input file list is empty", () => { - const f = { - path: "/a/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compiler: {}, - files: [] - }) - }; - const t1 = { - path: "/a/node_modules/@types/typings/index.d.ts", - content: `export * from "./lib"` - }; - const t2 = { - path: "/a/node_modules/@types/typings/lib.d.ts", - content: `export const x: number` - }; - const host = createWatchedSystem([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); - const watch = createWatchModeWithConfigFile(config.path, host); - - checkProgramActualFiles(watch(), [t1.path, t2.path]); - }); - - it("should support files without extensions", () => { - const f = { - path: "/a/compile", - content: "let x = 1" - }; - const host = createWatchedSystem([f, libFile]); - const watch = createWatchModeWithoutConfigFile([f.path], host, { allowNonTsExtensions: true }); - checkProgramActualFiles(watch(), [f.path, libFile.path]); - }); - - it("Options Diagnostic locations reported correctly with changes in configFile contents when options change", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFileContentBeforeComment = `{`; - const configFileContentComment = ` - // comment - // More comment`; - const configFileContentAfterComment = ` - "compilerOptions": { - "allowJs": true, - "declaration": true - } - }`; - const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; - const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; - const configFile = { - path: "/a/b/tsconfig.json", - content: configFileContentWithComment - }; - - const files = [file, libFile, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); - const errors = () => [ - getDiagnosticOfFile(watch().getCompilerOptions().configFile, configFile.content.indexOf('"allowJs"'), '"allowJs"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration"), - getDiagnosticOfFile(watch().getCompilerOptions().configFile, configFile.content.indexOf('"declaration"'), '"declaration"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration") - ]; - const intialErrors = errors(); - checkOutputErrors(host, intialErrors, /*isInitial*/ true); - - configFile.content = configFileContentWithoutCommentLine; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - const nowErrors = errors(); - checkOutputErrors(host, nowErrors); - assert.equal(nowErrors[0].start, intialErrors[0].start - configFileContentComment.length); - assert.equal(nowErrors[1].start, intialErrors[1].start - configFileContentComment.length); - }); - }); - - describe("tsc-watch emit with outFile or out setting", () => { - function createWatchForOut(out?: string, outFile?: string) { - const host = createWatchedSystem([]); - const config: FileOrFolderEmit = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { listEmittedFiles: true } - }) - }; - - let getOutput: (file: FileOrFolder) => string; - if (out) { - config.content = JSON.stringify({ - compilerOptions: { listEmittedFiles: true, out } - }); - getOutput = __ => getEmittedLineForSingleFileOutput(out, host); - } - else if (outFile) { - config.content = JSON.stringify({ - compilerOptions: { listEmittedFiles: true, outFile } - }); - getOutput = __ => getEmittedLineForSingleFileOutput(outFile, host); - } - else { - getOutput = file => getEmittedLineForMultiFileOutput(file, host); - } - - const f1 = getFileOrFolderEmit({ - path: "/a/a.ts", - content: "let x = 1" - }, getOutput); - const f2 = getFileOrFolderEmit({ - path: "/a/b.ts", - content: "let y = 1" - }, getOutput); - - const files = [f1, f2, config, libFile]; - host.reloadFS(files); - createWatchModeWithConfigFile(config.path, host); - - const allEmittedLines = getEmittedLines(files); - checkOutputContains(host, allEmittedLines); - host.clearOutput(); - - f1.content = "let x = 11"; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkAffectedLines(host, [f1], allEmittedLines); - } - - it("projectUsesOutFile should not be returned if not set", () => { - createWatchForOut(); - }); - - it("projectUsesOutFile should be true if out is set", () => { - const outJs = "/a/out.js"; - createWatchForOut(outJs); - }); - - it("projectUsesOutFile should be true if outFile is set", () => { - const outJs = "/a/out.js"; - createWatchForOut(/*out*/ undefined, outJs); - }); - - function verifyFilesEmittedOnce(useOutFile: boolean) { - const file1: FileOrFolder = { - path: "/a/b/output/AnotherDependency/file1.d.ts", - content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" - }; - const file2: FileOrFolder = { - path: "/a/b/dependencies/file2.d.ts", - content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" - }; - const file3: FileOrFolder = { - path: "/a/b/project/src/main.ts", - content: "namespace Main { export function fooBar() {} }" - }; - const file4: FileOrFolder = { - path: "/a/b/project/src/main2.ts", - content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" - }; - const configFile: FileOrFolder = { - path: "/a/b/project/tsconfig.json", - content: JSON.stringify({ + describe("emit once", () => { + function verifyFilesEmittedOnce(useOutFile: boolean) { + const configContent = JSON.stringify({ compilerOptions: useOutFile ? { outFile: "../output/common.js", target: "es5" } : { outDir: "../output", target: "es5" }, - files: [file1.path, file2.path, file3.path, file4.path] - }) - }; - const files = [file1, file2, file3, file4]; - const allfiles = files.concat(configFile); - const host = createWatchedSystem(allfiles); - const originalWriteFile = host.writeFile.bind(host); - const mapOfFilesWritten = createMap(); - host.writeFile = (p: string, content: string) => { - const count = mapOfFilesWritten.get(p); - mapOfFilesWritten.set(p, count ? count + 1 : 1); - return originalWriteFile(p, content); - }; - createWatchModeWithConfigFile(configFile.path, host); - if (useOutFile) { - // Only out file - assert.equal(mapOfFilesWritten.size, 1); - } - else { - // main.js and main2.js - assert.equal(mapOfFilesWritten.size, 2); - } - mapOfFilesWritten.forEach((value, key) => { - assert.equal(value, 1, "Key: " + key); - }); - } - - it("with --outFile and multiple declaration files in the program", () => { - verifyFilesEmittedOnce(/*useOutFile*/ true); - }); - - it("without --outFile and multiple declaration files in the program", () => { - verifyFilesEmittedOnce(/*useOutFile*/ false); - }); - }); - - describe("tsc-watch emit for configured projects", () => { - const file1Consumer1Path = "/a/b/file1Consumer1.ts"; - const moduleFile1Path = "/a/b/moduleFile1.ts"; - const configFilePath = "/a/b/tsconfig.json"; - interface InitialStateParams { - /** custom config file options */ - configObj?: any; - /** list of the files that will be emitted for first compilation */ - firstCompilationEmitFiles?: string[]; - /** get the emit file for file - default is multi file emit line */ - getEmitLine?(file: FileOrFolder, host: WatchedSystem): string; - /** Additional files and folders to add */ - getAdditionalFileOrFolder?(): FileOrFolder[]; - /** initial list of files to emit if not the default list */ - firstReloadFileList?: string[]; - } - function getInitialState({ configObj = {}, firstCompilationEmitFiles, getEmitLine, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { - const host = createWatchedSystem([]); - const getOutputName = getEmitLine ? (file: FileOrFolder) => getEmitLine(file, host) : - (file: FileOrFolder) => getEmittedLineForMultiFileOutput(file, host); - - const moduleFile1 = getFileOrFolderEmit({ - path: moduleFile1Path, - content: "export function Foo() { };", - }, getOutputName); - - const file1Consumer1 = getFileOrFolderEmit({ - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }, getOutputName); - - const file1Consumer2 = getFileOrFolderEmit({ - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }, getOutputName); - - const moduleFile2 = getFileOrFolderEmit({ - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;`, - }, getOutputName); - - const globalFile3 = getFileOrFolderEmit({ - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }); - - const additionalFiles = getAdditionalFileOrFolder ? - map(getAdditionalFileOrFolder(), file => getFileOrFolderEmit(file, getOutputName)) : - []; - - (configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true; - const configFile = getFileOrFolderEmit({ - path: configFilePath, - content: JSON.stringify(configObj) - }); - - const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; - let allEmittedFiles = getEmittedLines(files); - host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); - - // Initial compile - createWatchModeWithConfigFile(configFile.path, host); - if (firstCompilationEmitFiles) { - checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); - } - else { - checkOutputContains(host, allEmittedFiles); - } - host.clearOutput(); - - return { - moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, - files, - getFile, - verifyAffectedFiles, - verifyAffectedAllFiles, - getOutputName - }; - - function getFiles(filelist: string[]) { - return map(filelist, getFile); - } - - function getFile(fileName: string) { - return find(files, file => file.path === fileName); - } - - function verifyAffectedAllFiles() { - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputContains(host, allEmittedFiles); - host.clearOutput(); - } - - function verifyAffectedFiles(expected: FileOrFolderEmit[], filesToReload?: FileOrFolderEmit[]) { - if (!filesToReload) { - filesToReload = files; - } - else if (filesToReload.length > files.length) { - allEmittedFiles = getEmittedLines(filesToReload); - } - host.reloadFS(filesToReload); - host.checkTimeoutQueueLengthAndRun(1); - checkAffectedLines(host, expected, allEmittedFiles); - host.clearOutput(); - } - } - - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - verifyAffectedFiles - } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` - moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should be up-to-date with the reference map changes", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - verifyAffectedFiles - } = getInitialState(); - - // Change file1Consumer1 content to `export let y = Foo();` - file1Consumer1.content = `export let y = Foo();`; - verifyAffectedFiles([file1Consumer1]); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer2]); - - // Add the import statements back to file1Consumer1 - file1Consumer1.content = `import {Foo} from "./moduleFile1";let y = Foo();`; - verifyAffectedFiles([file1Consumer1]); - - // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` - moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer2, file1Consumer1]); - - // Multiple file edits in one go: - - // Change file1Consumer1 content to `export let y = Foo();` - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - file1Consumer1.content = `export let y = Foo();`; - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - }); - - it("should be up-to-date with deleted files", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - files, - verifyAffectedFiles - } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - - // Delete file1Consumer2 - const filesToLoad = mapDefined(files, file => file === file1Consumer2 ? undefined : file); - verifyAffectedFiles([moduleFile1, file1Consumer1], filesToLoad); - }); - - it("should be up-to-date with newly created files", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - files, - verifyAffectedFiles, - getOutputName - } = getInitialState(); - - const file1Consumer3 = getFileOrFolderEmit({ - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }, getOutputName); - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); - }); - - it("should detect changes in non-root files", () => { - const { - moduleFile1, file1Consumer1, - verifyAffectedFiles - } = getInitialState({ configObj: { files: [file1Consumer1Path] }, firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path] }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1]); - - // change file1 internal, and verify only file1 is affected - moduleFile1.content += "var T1: number;"; - verifyAffectedFiles([moduleFile1]); - }); - - it("should return all files if a global file changed shape", () => { - const { - globalFile3, verifyAffectedAllFiles - } = getInitialState(); - - globalFile3.content += "var T2: string;"; - verifyAffectedAllFiles(); - }); - - it("should always return the file itself if '--isolatedModules' is specified", () => { - const { - moduleFile1, verifyAffectedFiles - } = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const outFilePath = "/a/b/out.js"; - const { - moduleFile1, verifyAffectedFiles - } = getInitialState({ - configObj: { compilerOptions: { module: "system", outFile: outFilePath } }, - getEmitLine: (_, host) => getEmittedLineForSingleFileOutput(outFilePath, host) + files: [ + "/a/b/output/AnotherDependency/file1.d.ts", + "/a/b/dependencies/file2.d.ts", + "/a/b/project/src/main.ts", + "/a/b/project/src/main2.ts" + ] }); - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: FileOrFolder = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const { - moduleFile1, file1Consumer1, file1Consumer2, verifyAffectedFiles, getFile - } = getInitialState({ - getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] - }); - - const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path); - file1Consumer1.content += "export var T: number;"; - verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]); - - // Doesnt change the shape of file1Consumer1 - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - - // Change both files before the timeout - file1Consumer1.content += "export var T2: number;"; - moduleFile1.content = `export var T2: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1Emit]); - }); - - it("should work fine for files with circular references", () => { - // TODO: do not exit on such errors? Just continue to watch the files for update in watch mode - - const file1: FileOrFolder = { - path: "/a/b/file1.ts", - content: ` - /// - export var t1 = 10;` - }; - const file2: FileOrFolder = { - path: "/a/b/file2.ts", - content: ` - /// - export var t2 = 10;` - }; - const { - configFile, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [file1.path, file2.path], - getAdditionalFileOrFolder: () => [file1, file2], - firstReloadFileList: [libFile.path, file1.path, file2.path, configFilePath] - }); - const file1Emit = getFile(file1.path), file2Emit = getFile(file2.path); - - file1Emit.content += "export var t3 = 10;"; - verifyAffectedFiles([file1Emit, file2Emit], [file1, file2, libFile, configFile]); - - }); - - it("should detect removed code file", () => { - const referenceFile1: FileOrFolder = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { - configFile, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [referenceFile1.path, moduleFile1Path], - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [libFile.path, referenceFile1.path, moduleFile1Path, configFilePath] - }); - - const referenceFile1Emit = getFile(referenceFile1.path); - verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); - }); - - it("should detect non-existing code file", () => { - const referenceFile1: FileOrFolder = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { - configFile, - moduleFile2, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [referenceFile1.path], - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [libFile.path, referenceFile1.path, configFilePath] - }); - - const referenceFile1Emit = getFile(referenceFile1.path); - referenceFile1Emit.content += "export var yy = Foo();"; - verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); - - // Create module File2 and see both files are saved - verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]); - }); - }); - - describe("tsc-watch emit file content", () => { - interface EmittedFile extends FileOrFolder { - shouldBeWritten: boolean; - } - function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] { - return map(contents, (content, index) => { - return { - content, - path: changeExtension(files[index].path, Extension.Js), - shouldBeWritten: true + const filesWritten = new Map(); + const host = new class extends mocks.MockServerHost { + writeFile(path: string, data: string) { + filesWritten.set(path, (filesWritten.get(path) || 0) + 1); + super.writeFile(path, data); + } }; - } - ); - } - function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) { - for (const { path, content, shouldBeWritten } of emittedFiles) { - if (shouldBeWritten) { - assert.isTrue(host.fileExists(path), `Expected file ${path} to be present`); - assert.equal(host.readFile(path), content, `Contents of file ${path} do not match`); + + host.vfs.addFile("/a/b/output/AnotherDependency/file1.d.ts", `declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }`); + host.vfs.addFile("/a/b/dependencies/file2.d.ts", `declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }`); + host.vfs.addFile("/a/b/project/src/main.ts", `namespace Main { export function fooBar() { } }`); + host.vfs.addFile("/a/b/project/src/main2.ts", `namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }`); + host.vfs.addFile("/a/b/project/tsconfig.json", configContent); + + createWatchModeWithConfigFile("/a/b/project/tsconfig.json", host); + + if (useOutFile) { + // Only out file + assert.equal(filesWritten.size, 1); } else { - assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`); + // main.js and main2.js + assert.equal(filesWritten.size, 2); } + + filesWritten.forEach((value, key) => { + assert.equal(value, 1, "Key: " + key); + }); } - } - function verifyEmittedFileContents(newLine: string, inputFiles: FileOrFolder[], initialEmittedFileContents: string[], - modifyFiles: (files: FileOrFolderEmit[], emitedFiles: EmittedFile[]) => FileOrFolderEmit[], configFile?: FileOrFolder) { - const host = createWatchedSystem([], { newLine }); - const files = concatenate( - map(inputFiles, file => getFileOrFolderEmit(file, fileToConvert => getEmittedLineForMultiFileOutput(fileToConvert, host))), - configFile ? [libFile, configFile] : [libFile] - ); - const allEmittedFiles = getEmittedLines(files); - host.reloadFS(files); + it("with --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ true); + }); - // Initial compile - if (configFile) { - createWatchModeWithConfigFile(configFile.path, host); - } - else { - // First file as the root - createWatchModeWithoutConfigFile([files[0].path], host, { listEmittedFiles: true }); - } - checkOutputContains(host, allEmittedFiles); - - const emittedFiles = getEmittedFiles(files, initialEmittedFileContents); - verifyEmittedFiles(host, emittedFiles); - host.clearOutput(); - - const affectedFiles = modifyFiles(files, emittedFiles); - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkAffectedLines(host, affectedFiles, allEmittedFiles); - - verifyEmittedFiles(host, emittedFiles); - } - - function verifyNewLine(newLine: string) { - const lines = ["var x = 1;", "var y = 2;"]; - const fileContent = lines.join(newLine); - const f = { - path: "/a/app.ts", - content: fileContent - }; - - verifyEmittedFileContents(newLine, [f], [fileContent + newLine], modifyFiles); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content = fileContent + newLine + "var z = 3;"; - emittedFiles[0].content = files[0].content + newLine; - return [files[0]]; - } - } - - it("handles new lines: \\n", () => { - verifyNewLine("\n"); + it("without --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ false); + }); }); - it("handles new lines: \\r\\n", () => { - verifyNewLine("\r\n"); - }); - - it("should emit specified file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export function Foo() { return 10; }` - }; - - const file2 = { - path: "/a/b/f2.ts", - content: `import {Foo} from "./f1"; export let y = Foo();` - }; - - const file3 = { - path: "/a/b/f3.ts", - content: `import {y} from "./f2"; let x = y;` - }; - - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } }) - }; - - verifyEmittedFileContents("\r\n", [file1, file2, file3], [ - `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`, - `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, - `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n` - ], modifyFiles, configFile); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content += `export function foo2() { return 2; }`; - emittedFiles[0].content += `function foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`; - emittedFiles[2].shouldBeWritten = false; - return files.slice(0, 2); + describe("emit", () => { + function writeFile(host: mocks.MockServerHost, path: string, content: string) { + host.vfs.writeFile(path, content); } - }); - it("Elides const enums correctly in incremental compilation", () => { - const currentDirectory = "/user/someone/projects/myproject"; - const file1: FileOrFolder = { - path: `${currentDirectory}/file1.ts`, - content: "export const enum E1 { V = 1 }" - }; - const file2: FileOrFolder = { - path: `${currentDirectory}/file2.ts`, - content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` - }; - const file3: FileOrFolder = { - path: `${currentDirectory}/file3.ts`, - content: `import { E2 } from "./file2"; const v: E2 = E2.V;` - }; - const strictAndEsModule = `"use strict";\nexports.__esModule = true;\n`; - verifyEmittedFileContents("\n", [file3, file2, file1], [ - `${strictAndEsModule}var v = 1 /* V */;\n`, - strictAndEsModule, - strictAndEsModule - ], modifyFiles); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content += `function foo2() { return 2; }`; - emittedFiles[0].content += `function foo2() { return 2; }\n`; - emittedFiles[1].shouldBeWritten = false; - emittedFiles[2].shouldBeWritten = false; - return [files[0]]; + function writeConfigFile(host: mocks.MockServerHost, path: string, config: any = {}) { + const compilerOptions = (config.compilerOptions || (config.compilerOptions = {})); + compilerOptions.listEmittedFiles = true; + writeFile(host, path, JSON.stringify(config)); } + + function waitAndCheckAffectedFiles(host: mocks.MockServerHost, affectedFiles: ReadonlyArray, unaffectedFiles?: ReadonlyArray) { + host.checkTimeoutQueueLengthAndRun(1); + checkAffectedFiles(host, affectedFiles, unaffectedFiles); + } + + function checkAffectedFiles(host: mocks.MockServerHost, affectedFiles: ReadonlyArray, unaffectedFiles?: ReadonlyArray) { + affectedFiles = getEmittedLines(affectedFiles, host, formatOutputFile); + checkOutputContains(host, affectedFiles); + if (unaffectedFiles) { + unaffectedFiles = getEmittedLines(unaffectedFiles, host, formatOutputFile); + unaffectedFiles = mapDefined(unaffectedFiles, line => contains(affectedFiles, line) ? undefined : line); + checkOutputDoesNotContain(host, unaffectedFiles); + } + host.clearOutput(); + } + + describe("affected files", () => { + describe("with --outFile or --out", () => { + const configFilePath = "/a/tsconfig.json"; + const file1Path = "/a/a.ts"; + const file1OutputPath = "/a/a.js"; + const file2Path = "/a/b.ts"; + const file2OutputPath = "/a/b.js"; + const commonOutputPaths: ReadonlyArray = [file1OutputPath, file2OutputPath]; + + function writeCommonFiles(host: mocks.MockServerHost) { + writeFile(host, file1Path, `let x = 1`); + writeFile(host, file2Path, `let y = 1`); + } + + it("if neither is set", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + writeFile(host, file1Path, `let x = 11`); + waitAndCheckAffectedFiles(host, [file1OutputPath], commonOutputPaths); + }); + + it("if --out is set", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath, { compilerOptions: { out: "/a/out.js" } }); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, ["/a/out.js"], commonOutputPaths); + + writeFile(host, file1Path, `let x = 11`); + waitAndCheckAffectedFiles(host, ["/a/out.js"], commonOutputPaths); + }); + + it("if --outFile is set", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath, { compilerOptions: { outFile: "/a/out.js" } }); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, ["/a/out.js"], commonOutputPaths); + + writeFile(host, file1Path, `let x = 11`); + waitAndCheckAffectedFiles(host, ["/a/out.js"], commonOutputPaths); + }); + + }); + + describe("for configured projects", () => { + const configFilePath = "/a/b/tsconfig.json"; + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const file1Consumer1OutputPath = "/a/b/file1Consumer1.js"; + const file1Consumer2Path = "/a/b/file1Consumer2.ts"; + const file1Consumer2OutputPath = "/a/b/file1Consumer2.js"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const moduleFile1OutputPath = "/a/b/moduleFile1.js"; + const moduleFile2Path = "/a/b/moduleFile2.ts"; + const moduleFile2OutputPath = "/a/b/moduleFile2.js"; + const globalFile3Path = "/a/b/globalFile3.ts"; + const globalFile3OutputPath = "/a/b/globalFile3.js"; + const commonOutputPaths: ReadonlyArray = [ + file1Consumer1OutputPath, + file1Consumer2OutputPath, + moduleFile1OutputPath, + moduleFile2OutputPath, + globalFile3OutputPath + ]; + + function writeCommonFiles(host: mocks.MockServerHost, files?: string[]) { + if (!files || ts.contains(files, moduleFile1Path)) { + writeFile(host, moduleFile1Path, `export function Foo() { };`); + } + if (!files || ts.contains(files, moduleFile2Path)) { + writeFile(host, moduleFile2Path, `export var Foo4 = 10;`); + } + if (!files || ts.contains(files, file1Consumer1Path)) { + writeFile(host, file1Consumer1Path, `import {Foo} from "./moduleFile1"; export var y = 10;`); + } + if (!files || ts.contains(files, file1Consumer2Path)) { + writeFile(host, file1Consumer2Path, `import {Foo} from "./moduleFile1"; let z = 10;`); + } + if (!files || ts.contains(files, globalFile3Path)) { + writeFile(host, globalFile3Path, `interface GlobalFoo { age: number }`); + } + } + + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + // Make a change to moduleFile1 that changes its external shape + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath, file1Consumer2OutputPath], commonOutputPaths); + + // Make a change to moduleFile1 that does not change its external shape + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { console.log('hi'); };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath], commonOutputPaths); + }); + + it("should be up-to-date with the reference map changes", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + // Remove import of moduleFile1 from file1Consumer1. Should only affect itself. + writeFile(host, file1Consumer1Path, `export let y = Foo();`); + waitAndCheckAffectedFiles(host, [file1Consumer1OutputPath], commonOutputPaths); + + // Add additional export to moduleFile1. Should not affect file1Consumer1 + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer2OutputPath], commonOutputPaths); + + // Restore import of moduleFile1 to file1Consumer1. Should only affect itself. + writeFile(host, file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`); + waitAndCheckAffectedFiles(host, [file1Consumer1OutputPath], commonOutputPaths); + + // Add additional export to moduleFile1. Should now also affect file1Consumer1. + writeFile(host, moduleFile1Path, `export var T: number;export var T2: string;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath, file1Consumer2OutputPath], commonOutputPaths); + + // Multiple file edits in one go. + writeFile(host, file1Consumer1Path, `export let y = Foo();`); + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath, file1Consumer2OutputPath], commonOutputPaths); + }); + + it("should be up-to-date with deleted files", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + + // Delete file1Consumer2 + host.vfs.removeFile(file1Consumer2Path); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath], commonOutputPaths); + }); + + it("should be up-to-date with newly created files", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + writeFile(host, "/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath, "/a/b/file1Consumer3.js", file1Consumer2OutputPath], commonOutputPaths); + }); + + it("should detect changes in non-root files", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host, [file1Consumer1Path, moduleFile1Path]); + writeConfigFile(host, configFilePath, { files: [file1Consumer1Path] }); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, [file1Consumer1OutputPath, moduleFile1OutputPath]); + + // Add export to moduleFile1. Should affect moduleFile1 and file1Consumer1. + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath]); + + // Change moduleFile1 internal. Should only affect moduleFile1. + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };var T1: number;`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath], [file1Consumer1OutputPath]); + }); + + it("should return all files if a global file changed shape", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + // Add declaration to global. Should affect all files. + writeFile(host, globalFile3Path, `interface GlobalFoo { age: number }\nvar T2: string;`); + waitAndCheckAffectedFiles(host, commonOutputPaths); + }); + + it("should always return the file itself if '--isolatedModules' is specified", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath, { compilerOptions: { isolatedModules: true } }); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + // Add export to moduleFile1. Should only affect moduleFile1. + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath], commonOutputPaths); + }); + + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath, { compilerOptions: { module: "system", outFile: "/a/b/out.js" } }); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, ["/a/b/out.js"]); + + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, ["/a/b/out.js"]); + }); + + it("should return cascaded affected file list", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeCommonFiles(host); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, commonOutputPaths); + + writeFile(host, "/a/b/file1Consumer1Consumer1.ts", `import {y} from "./file1Consumer1";`); + writeFile(host, file1Consumer1Path, `import {Foo} from "./moduleFile1"; export var y = 10; export var T: number;`) + waitAndCheckAffectedFiles(host, [file1Consumer1OutputPath, "/a/b/file1Consumer1Consumer1.js"], commonOutputPaths); + + // Doesn't change the shape of file1Consumer1 + writeFile(host, moduleFile1Path, `export var T: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath, file1Consumer2OutputPath], commonOutputPaths); + + // Change both files before the timeout + writeFile(host, file1Consumer1Path, `import {Foo} from "./moduleFile1"; export var y = 10; export var T: number; export var T2: number;`) + writeFile(host, moduleFile1Path, `export var T2: number;export function Foo() { };`); + waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath, file1Consumer2OutputPath, "/a/b/file1Consumer1Consumer1.js"], commonOutputPaths); + }); + + it("should work fine for files with circular references", () => { + // TODO: do not exit on such errors? Just continue to watch the files for update in watch mode + const host = new mocks.MockServerHost({ lib: true }); + writeFile(host, "/a/b/file1.ts", `/// \nexport var t1 = 10;`); + writeFile(host, "/a/b/file2.ts", `/// \nexport var t2 = 10;`); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, ["/a/b/file1.js", "/a/b/file2.js"]); + + writeFile(host, "/a/b/file1.ts", `/// \nexport var t1 = 10;\nexport var t3 = 10;`); + waitAndCheckAffectedFiles(host, ["/a/b/file1.js", "/a/b/file2.js"]); + }); + + it("should detect removed code file", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeFile(host, "/a/b/referenceFile1.ts", `/// \nexport var x = Foo();`); + writeCommonFiles(host, [moduleFile1Path]); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, ["/a/b/referenceFile1.js", moduleFile1OutputPath]); + + host.vfs.removeFile(moduleFile1Path); + waitAndCheckAffectedFiles(host, ["/a/b/referenceFile1.js"], [moduleFile1OutputPath]); + }); + + it("should detect non-existing code file", () => { + const host = new mocks.MockServerHost({ lib: true }); + writeFile(host, "/a/b/referenceFile1.ts", `/// \nexport var x = Foo();`); + writeConfigFile(host, configFilePath); + + createWatchModeWithConfigFile(configFilePath, host); + checkAffectedFiles(host, ["/a/b/referenceFile1.js"]); + + writeFile(host, "/a/b/referenceFile1.ts", `/// \nexport var x = Foo();\nexport var yy = Foo();`); + waitAndCheckAffectedFiles(host, ["/a/b/referenceFile1.js"]); + + writeCommonFiles(host, [moduleFile2Path]); + waitAndCheckAffectedFiles(host, ["/a/b/referenceFile1.js", moduleFile2OutputPath]); + }); + }); + }); + + describe("file content", () => { + theory("handles new lines", [ + { title: "\\r\\n", args: ["\r\n"] }, + { title: "\\n", args: ["\n"] } + ], (newLine: "\r\n" | "\n") => { + const host = new mocks.MockServerHost({ newLine }); + writeFile(host, "/a/app.ts", `var x = 1;${newLine}var y = 2;`); + + createWatchModeWithoutConfigFile(["/a/app.ts"], host, { listEmittedFiles: true }); + checkAffectedFiles(host, ["/a/app.js"]); + + assert.isTrue(host.fileExists("/a/app.js")); + assert.strictEqual(host.readFile("/a/app.js"), `var x = 1;${newLine}var y = 2;${newLine}`); + + writeFile(host, "/a/app.ts", `var x = 1;${newLine}var y = 2;${newLine}var z = 3;`); + waitAndCheckAffectedFiles(host, ["/a/app.js"]); + + assert.isTrue(host.fileExists("/a/app.js")); + assert.strictEqual(host.readFile("/a/app.js"), `var x = 1;${newLine}var y = 2;${newLine}var z = 3;${newLine}`); + }); + + it("should emit specified file", () => { + const filesWritten = new Set(); + const host = new class extends mocks.MockServerHost { + writeFile(path: string, content: string) { + filesWritten.add(path); + super.writeFile(path, content); + } + }({ newLine: "\r\n" }); + + writeFile(host, "/a/b/f1.ts", `export function Foo() { return 10; }`); + writeFile(host, "/a/b/f2.ts", `import {Foo} from "./f1"; export let y = Foo();`); + writeFile(host, "/a/b/f3.ts", `import {y} from "./f2"; let x = y;`); + writeConfigFile(host, "/a/b/tsconfig.json"); + + createWatchModeWithConfigFile("/a/b/tsconfig.json", host); + checkAffectedFiles(host, ["/a/b/f1.js", "/a/b/f2.js", "/a/b/f3.js"]); + + assert.isTrue(filesWritten.has("/a/b/f1.js")); + assert.strictEqual(host.readFile("/a/b/f1.js"), `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`); + + assert.isTrue(filesWritten.has("/a/b/f2.js")); + assert.strictEqual(host.readFile("/a/b/f2.js"), `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`); + + assert.isTrue(filesWritten.has("/a/b/f3.js")); + assert.strictEqual(host.readFile("/a/b/f3.js"), `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n`); + + filesWritten.clear(); + + writeFile(host, "/a/b/f1.ts", `export function Foo() { return 10; }export function foo2() { return 2; }`); + waitAndCheckAffectedFiles(host, ["/a/b/f1.js", "/a/b/f2.js"], ["/a/b/f3.js"]); + + assert.isTrue(filesWritten.has("/a/b/f1.js")); + assert.strictEqual(host.readFile("/a/b/f1.js"), `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\nfunction foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`); + + assert.isTrue(filesWritten.has("/a/b/f2.js")); + assert.strictEqual(host.readFile("/a/b/f2.js"), `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`); + + assert.isFalse(filesWritten.has("/a/b/f3.js")); + }); + + it("Elides const enums correctly in incremental compilation", () => { + const filesWritten = new Set(); + const host = new class extends mocks.MockServerHost { + writeFile(path: string, content: string) { + filesWritten.add(path); + super.writeFile(path, content); + } + }({ lib: true, newLine: "\n" }); + + writeFile(host, "/user/someone/projects/myproject/file1.ts", `export const enum E1 { V = 1 }`); + writeFile(host, "/user/someone/projects/myproject/file2.ts", `import { E1 } from "./file1"; export const enum E2 { V = E1.V }`); + writeFile(host, "/user/someone/projects/myproject/file3.ts", `import { E2 } from "./file2"; const v: E2 = E2.V;`); + + createWatchModeWithoutConfigFile(["/user/someone/projects/myproject/file1.ts", "/user/someone/projects/myproject/file2.ts", "/user/someone/projects/myproject/file3.ts"], host, { listEmittedFiles: true }); + checkAffectedFiles(host, ["/user/someone/projects/myproject/file1.js", "/user/someone/projects/myproject/file2.js", "/user/someone/projects/myproject/file3.js"]); + + assert.isTrue(filesWritten.has("/user/someone/projects/myproject/file1.js")); + assert.strictEqual(host.readFile("/user/someone/projects/myproject/file1.js"), `"use strict";\nexports.__esModule = true;\n`); + + assert.isTrue(filesWritten.has("/user/someone/projects/myproject/file2.js")); + assert.strictEqual(host.readFile("/user/someone/projects/myproject/file2.js"), `"use strict";\nexports.__esModule = true;\n`); + + assert.isTrue(filesWritten.has("/user/someone/projects/myproject/file3.js")); + assert.strictEqual(host.readFile("/user/someone/projects/myproject/file3.js"), `"use strict";\nexports.__esModule = true;\nvar v = 1 /* V */;\n`); + + filesWritten.clear(); + + writeFile(host, "/user/someone/projects/myproject/file1.ts", `export const enum E1 { V = 1 }function foo2() { return 2; }`); + waitAndCheckAffectedFiles(host, ["/user/someone/projects/myproject/file1.js"], ["/user/someone/projects/myproject/file2.js", "/user/someone/projects/myproject/file3.js"]); + + assert.isTrue(filesWritten.has("/user/someone/projects/myproject/file1.js")); + assert.strictEqual(host.readFile("/user/someone/projects/myproject/file1.js"), `"use strict";\nexports.__esModule = true;\nfunction foo2() { return 2; }\n`); + + assert.isFalse(filesWritten.has("/user/someone/projects/myproject/file2.js")); + assert.isFalse(filesWritten.has("/user/someone/projects/myproject/file3.js")); + }); + }); }); - }); - describe("tsc-watch module resolution caching", () => { - it("works", () => { - const root = { - path: "/a/d/f0.ts", - content: `import {x} from "f1"` - }; - const imported = { - path: "/a/f1.ts", - content: `foo()` - }; + describe("module resolution caching", () => { + it("works", () => { + const rootContent1 = `import {x} from "f1"`; + const importedContent = `foo()`; - const files = [root, imported, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/d/f0.ts", rootContent1); + host.vfs.addFile("/a/f1.ts", importedContent); - const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); - const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); + const fileExists = host.fileExists; + const watch = createWatchModeWithoutConfigFile(["/a/d/f0.ts"], host, { module: ModuleKind.AMD }); - // ensure that imported file was found - checkOutputErrors(host, [f1IsNotModule, cannotFindFoo], /*isInitial*/ true); + // ensure that imported file was found + checkOutputErrors(host, [ + createFileIsNotAModuleDiagnostic(watch(), "/a/d/f0.ts", rootContent1, "f1", "/a/f1.ts"), + createCannotFindNameDiagnostic(watch(), "/a/f1.ts", importedContent, "foo") + ], /*isInitial*/ true); - const originalFileExists = host.fileExists; - { - const newContent = `import {x} from "f1" - var x: string = 1;`; - root.content = newContent; - host.reloadFS(files); + // spy on calls to fileExists to make sure that disk is not touched + const fileExistsSpy1 = new Spy(fileExists); + host.fileExists = fileExistsSpy1.value; - // patch fileExists to make sure that disk is not touched - host.fileExists = notImplemented; - - // trigger synchronization to make sure that import will be fetched from the cache + // write file and trigger synchronization + const rootContent2 = `import {x} from "f1"\nvar x: string = 1;`; + host.vfs.writeFile("/a/d/f0.ts", rootContent2); host.runQueuedTimeoutCallbacks(); + // verify fileExists was not called. + fileExistsSpy1.verify(_ => _(Arg.any()), Times.none()); + // ensure file has correct number of errors after edit checkOutputErrors(host, [ - f1IsNotModule, - getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), - cannotFindFoo + createFileIsNotAModuleDiagnostic(watch(), "/a/d/f0.ts", rootContent1, "f1", "/a/f1.ts"), + createFileDiagnostic(getFile(watch(), "/a/d/f0.ts"), rootContent2.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, "1", "string"), + createCannotFindNameDiagnostic(watch(), "/a/f1.ts", importedContent, "foo") ]); - } - { - let fileExistsIsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(host, fileName); - }; - root.content = `import {x} from "f2"`; - host.reloadFS(files); + // spy on calls to fileExists to make sure LSHost only searches for 'f2' + const fileExistsSpy2 = new Spy(fileExists); + host.fileExists = fileExistsSpy2.value; - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + // write file and trigger synchronization + const rootContent3 = `import {x} from "f2"`; + host.vfs.writeFile("/a/d/f0.ts", rootContent3); host.runQueuedTimeoutCallbacks(); + // verify fileExists was called correctly + fileExistsSpy2 + .verify(_ => _(Arg.includes("/f2.")), Times.atLeastOnce()) + .verify(_ => _(Arg.not(Arg.includes("/f2."))), Times.none()); + // ensure file has correct number of errors after edit checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") + createCannotFindModuleDiagnostic(watch(), "/a/d/f0.ts", rootContent3, "f2") ]); - assert.isTrue(fileExistsIsCalled); - } - { - let fileExistsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(host, fileName); - }; + // spy on calls to fileExists to make sure LSHost only searches for 'f1' + const fileExistsSpy3 = new Spy(fileExists); + host.fileExists = fileExistsSpy3.value; - const newContent = `import {x} from "f1"`; - root.content = newContent; - - host.reloadFS(files); + // write file and trigger synchronization + const rootContent4 = `import {x} from "f1"`; + host.vfs.writeFile("/a/d/f0.ts", rootContent4); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [f1IsNotModule, cannotFindFoo]); - assert.isTrue(fileExistsCalled); - } - }); + // verify fileExists was called correctly + fileExistsSpy3 + .verify(_ => _(Arg.includes("/f1.")), Times.atLeastOnce()) + .verify(_ => _(Arg.not(Arg.includes("/f1."))), Times.none()); - it("loads missing files from disk", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; + checkOutputErrors(host, [ + createFileIsNotAModuleDiagnostic(watch(), "/a/d/f0.ts", rootContent1, "f1", "/a/f1.ts"), + createCannotFindNameDiagnostic(watch(), "/a/f1.ts", importedContent, "foo") + ]); + }); - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;` - }; + it("loads missing files from disk", () => { + const rootContent1 = `import {x} from "bar"`; - const files = [root, libFile]; - const host = createWatchedSystem(files); - const originalFileExists = host.fileExists; + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/foo.ts", rootContent1); - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } + const fileExists = host.fileExists; - return originalFileExists.call(host, fileName); - }; + // spy on calls to fileExists when starting watch mode + const fileExistsSpy1 = new Spy(fileExists); + host.fileExists = fileExistsSpy1.value; - const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + const watch = createWatchModeWithoutConfigFile(["/a/foo.ts"], host, { module: ModuleKind.AMD }); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ], /*isInitial*/ true); + // verify fileExists was called correctly + fileExistsSpy1.verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()); - fileExistsCalledForBar = false; - root.content = `import {y} from "bar"`; - host.reloadFS(files.concat(imported)); + checkOutputErrors(host, [ + createCannotFindModuleDiagnostic(watch(), "/a/foo.ts", rootContent1, "bar") + ], /*isInitial*/ true); - host.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrors(host, emptyArray); - }); + // spy on calls to fileExists after synchronization is triggered + const fileExistsSpy2 = new Spy(fileExists); + host.fileExists = fileExistsSpy2.value; - it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; + host.vfs.writeFile("/a/foo.ts", `import {y} from "bar"`); + host.vfs.writeFile("/a/bar.d.ts", `export const y = 1;`); + host.runQueuedTimeoutCallbacks(); - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;export const x = 10;` - }; + // verify fileExists was called correctly + fileExistsSpy2.verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()); - const files = [root, libFile]; - const filesWithImported = files.concat(imported); - const host = createWatchedSystem(filesWithImported); - const originalFileExists = host.fileExists; - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - return originalFileExists.call(host, fileName); - }; + checkOutputErrors(host, emptyArray); + }); - const watch = createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { + const rootContent = `import {x} from "bar"`; + const importedContent = `export const y = 1;export const x = 10;`; - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrors(host, emptyArray, /*isInitial*/ true); + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/foo.ts", rootContent); + host.vfs.addFile("/a/bar.d.ts", importedContent); - fileExistsCalledForBar = false; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); + const fileExists = host.fileExists; - fileExistsCalledForBar = false; - host.reloadFS(filesWithImported); - host.checkTimeoutQueueLengthAndRun(1); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrors(host, emptyArray); - }); + // spy on fileExists when starting watch mode + const fileExistsSpy1 = new Spy(fileExists); + host.fileExists = fileExistsSpy1.value; - it("works when module resolution changes to ambient module", () => { - const root = { - path: "/a/b/foo.ts", - content: `import * as fs from "fs";` - }; + const watch = createWatchModeWithoutConfigFile(["/a/foo.ts"], host, { module: ModuleKind.AMD }); - const packageJson = { - path: "/a/b/node_modules/@types/node/package.json", - content: ` -{ - "main": "" -} -` - }; + // verify fileExists was called correctly + fileExistsSpy1.verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()); - const nodeType = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -}` - }; + checkOutputErrors(host, emptyArray, /*isInitial*/ true); - const files = [root, libFile]; - const filesWithNodeType = files.concat(packageJson, nodeType); - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + // spy on fileExists when triggering synchronization + const fileExistsSpy2 = new Spy(fileExists); + host.fileExists = fileExistsSpy2.value; - const watch = createWatchModeWithoutConfigFile([root.path], host, { }); + host.vfs.removeFile("/a/bar.d.ts"); + host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ], /*isInitial*/ true); + // verify fileExists was called correctly + fileExistsSpy2.verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()); - host.reloadFS(filesWithNodeType); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray); - }); + checkOutputErrors(host, [ + createCannotFindModuleDiagnostic(watch(), "/a/foo.ts", rootContent, "bar") + ]); - it("works when included file with ambient module changes", () => { - const root = { - path: "/a/b/foo.ts", - content: ` -import * as fs from "fs"; -import * as u from "url"; -` - }; + // spy on fileExists when triggering synchronization + const fileExistsSpy3 = new Spy(fileExists); + host.fileExists = fileExistsSpy3.value; - const file = { - path: "/a/b/bar.d.ts", - content: ` -declare module "url" { - export interface Url { - href?: string; - } -} -` - }; + host.vfs.writeFile("/a/bar.d.ts", importedContent);; + host.checkTimeoutQueueLengthAndRun(1); - const fileContentWithFS = ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -} -`; + // verify fileExists was called correctly. + fileExistsSpy3.verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()); - const files = [root, file, libFile]; - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + checkOutputErrors(host, emptyArray); + }); - const watch = createWatchModeWithoutConfigFile([root.path, file.path], host, {}); + it("works when module resolution changes to ambient module", () => { + const rootContent = `import * as fs from "fs";`; + const host = new mocks.MockServerHost({ vfs: { currentDirectory: "/a/b" }, lib: true }); + host.vfs.addFile("/a/b/foo.ts", rootContent); - checkOutputErrors(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ], /*isInitial*/ true); + const watch = createWatchModeWithoutConfigFile(["/a/b/foo.ts"], host, { }); - file.content += fileContentWithFS; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray); - }); + checkOutputErrors(host, [ + createCannotFindModuleDiagnostic(watch(), "/a/b/foo.ts", rootContent, "fs") + ], /*isInitial*/ true); - it("works when reusing program with files from external library", () => { - interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } - const configDir = "/a/b/projects/myProject/src/"; - const file1: FileOrFolder = { - path: configDir + "file1.ts", - content: 'import module1 = require("module1");\nmodule1("hello");' - }; - const file2: FileOrFolder = { - path: configDir + "file2.ts", - content: 'import module11 = require("module1");\nmodule11("hello");' - }; - const module1: FileOrFolder = { - path: "/a/b/projects/myProject/node_modules/module1/index.js", - content: "module.exports = options => { return options.toString(); }" - }; - const configFile: FileOrFolder = { - path: configDir + "tsconfig.json", - content: JSON.stringify({ + host.vfs.writeFile("/a/b/node_modules/@types/node/package.json", `{\n "main": ""\n}\n`); + host.vfs.writeFile("/a/b/node_modules/@types/node/index.d.ts", `\ndeclare module "fs" {\n export interface Stats {\n isFile(): boolean;\n }\n}`); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray); + }); + + it("works when included file with ambient module changes", () => { + const rootContent = `import * as fs from "fs";\nimport * as u from "url";`; + const fileContent1 = `declare module "url" {\n export interface Url {\n href?: string;\n }\n}`; + + const host = new mocks.MockServerHost({ vfs: { currentDirectory: "/a/b" }, lib: true }); + host.vfs.addFile("/a/b/foo.ts", rootContent); + host.vfs.addFile("/a/b/bar.d.ts", fileContent1); + + const watch = createWatchModeWithoutConfigFile(["/a/b/foo.ts", "/a/b/bar.d.ts"], host, {}); + + checkOutputErrors(host, [ + createCannotFindModuleDiagnostic(watch(), "/a/b/foo.ts", rootContent, "fs") + ], /*isInitial*/ true); + + const fileContent2 = fileContent1 + `declare module "fs" {\n export interface Stats {\n isFile(): boolean;\n }\n}`; + host.vfs.writeFile("/a/b/bar.d.ts", fileContent2); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray); + }); + + it("works when reusing program with files from external library", () => { + const file1Content = `import module1 = require("module1");\nmodule1("hello");`; + const file1Output = `"use strict";\nexports.__esModule = true;\nvar module1 = require("module1");\nmodule1("hello");\n`; + const file2Content = `import module11 = require("module1");\nmodule11("hello");`; + const file2Output = `"use strict";\nexports.__esModule = true;\nvar module11 = require("module1");\nmodule11("hello");\n`; + + const host = new mocks.MockServerHost({ vfs: { currentDirectory: "/a/b/projects/myProject/" }, lib: true }); + host.vfs.addFile("/a/b/projects/myProject/src/file1.ts", file1Content); + host.vfs.addFile("/a/b/projects/myProject/src/file2.ts", file2Content); + host.vfs.addFile("/a/b/projects/myProject/node_modules/module1/index.js", `module.exports = options => { return options.toString(); }`); + host.vfs.addFile("/a/b/projects/myProject/src/tsconfig.json", JSON.stringify({ compilerOptions: { allowJs: true, rootDir: ".", @@ -1932,67 +1380,67 @@ declare module "fs" { moduleResolution: "node", maxNodeModuleJsDepth: 1 } - }) - }; - const outDirFolder = "/a/b/projects/myProject/dist/"; - const programFiles = [file1, file2, module1, libFile]; - const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); - const watch = createWatchModeWithConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray, /*isInitial*/ true); - const expectedFiles: ExpectedFile[] = [ - createExpectedEmittedFile(file1), - createExpectedEmittedFile(file2), - createExpectedToNotEmitFile("index.js"), - createExpectedToNotEmitFile("src/index.js"), - createExpectedToNotEmitFile("src/file1.js"), - createExpectedToNotEmitFile("src/file2.js"), - createExpectedToNotEmitFile("lib.js"), - createExpectedToNotEmitFile("lib.d.ts") - ]; - verifyExpectedFiles(expectedFiles); + })); - file1.content += "\n;"; - expectedFiles[0].content += ";\n"; // Only emit file1 with this change - expectedFiles[1].isExpectedToEmit = false; - host.reloadFS(programFiles.concat(configFile)); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray); - verifyExpectedFiles(expectedFiles); + const writeFile = host.writeFile; + // spy on calls to writeFile when starting watch mode + const writeFileSpy1 = new Spy(writeFile); + host.writeFile = writeFileSpy1.value; - function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { - forEach(expectedFiles, f => { - assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); - if (f.isExpectedToEmit) { - assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); - } - }); - } + const watch = createWatchModeWithConfigFile("/a/b/projects/myProject/src/tsconfig.json", host); + checkProgramActualFiles(watch(), [ + "/a/b/projects/myProject/src/file1.ts", + "/a/b/projects/myProject/src/file2.ts", + "/a/b/projects/myProject/node_modules/module1/index.js", + mocks.MockServerHost.libPath + ]); + checkOutputErrors(host, emptyArray, /*isInitial*/ true); - function createExpectedToNotEmitFile(fileName: string): ExpectedFile { - return { - path: outDirFolder + fileName, - isExpectedToEmit: false - }; - } + // verify writeFile was called correctly. + writeFileSpy1 + .verify(_ => _("/a/b/projects/myProject/dist/file1.js", file1Output, Arg.any()), Times.once()) + .verify(_ => _("/a/b/projects/myProject/dist/file2.js", file2Output, Arg.any()), Times.once()) + .verify(_ => _("/a/b/projects/myProject/dist/index.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/src/index.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/src/file1.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/src/file2.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/lib.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/lib.d.ts", Arg.string(), Arg.any()), Times.none()); - function createExpectedEmittedFile(file: FileOrFolder): ExpectedFile { - return { - path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, - isExpectedToEmit: true, - content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" - }; - } + // spy on calls to writeFile when triggering synchronization + const writeFileSpy2 = new Spy(writeFile); + host.writeFile = writeFileSpy2.value; + + host.vfs.writeFile("/a/b/projects/myProject/src/file1.ts", file1Content + "\n;"); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [ + "/a/b/projects/myProject/src/file1.ts", + "/a/b/projects/myProject/src/file2.ts", + "/a/b/projects/myProject/node_modules/module1/index.js", + mocks.MockServerHost.libPath + ]); + checkOutputErrors(host, emptyArray); + + // verify writeFile was called correctly + writeFileSpy2 + .verify(_ => _("/a/b/projects/myProject/dist/file1.js", file1Output + ";\n", Arg.any()), Times.once()) + .verify(_ => _("/a/b/projects/myProject/dist/file2.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/index.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/src/index.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/src/file1.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/src/file2.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/lib.js", Arg.string(), Arg.any()), Times.none()) + .verify(_ => _("/a/b/projects/myProject/dist/lib.d.ts", Arg.string(), Arg.any()), Times.none()); + }); }); - }); - describe("tsc-watch with when module emit is specified as node", () => { - it("when instead of filechanged recursive directory watcher is invoked", () => { - const configFile: FileOrFolder = { - path: "/a/rootFolder/project/tsconfig.json", - content: JSON.stringify({ + describe("with when module emit is specified as node", () => { + it("when instead of filechanged recursive directory watcher is invoked", () => { + const host = new mocks.MockServerHost({ lib: true }); + host.vfs.addFile("/a/rootFolder/project/Scripts/TypeScript.ts", `var z = 10;`); + host.vfs.addFile("/a/rootFolder/project/Scripts/Javascript.js", `var zz = 10;`); + host.vfs.addFile("/a/rootFolder/project/tsconfig.json", JSON.stringify({ compilerOptions: { module: "none", allowJs: true, @@ -2001,30 +1449,36 @@ declare module "fs" { include: [ "Scripts/**/*" ], - }) - }; - const outputFolder = "/a/rootFolder/project/Static/scripts/"; - const file1: FileOrFolder = { - path: "/a/rootFolder/project/Scripts/TypeScript.ts", - content: "var z = 10;" - }; - const file2: FileOrFolder = { - path: "/a/rootFolder/project/Scripts/Javascript.js", - content: "var zz = 10;" - }; - const files = [configFile, file1, file2, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchModeWithConfigFile(configFile.path, host); + })); - checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); - file1.content = "var zz30 = 100;"; - host.reloadFS(files, { invokeDirectoryWatcherInsteadOfFileChanged: true }); - host.runQueuedTimeoutCallbacks(); + const writeFile = host.writeFile; + const watch = createWatchModeWithConfigFile("/a/rootFolder/project/tsconfig.json", host); - checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); - const outputFile1 = changeExtension((outputFolder + getBaseFileName(file1.path)), ".js"); - assert.isTrue(host.fileExists(outputFile1)); - assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + checkProgramActualFiles(watch(), [ + "/a/rootFolder/project/Scripts/TypeScript.ts", + "/a/rootFolder/project/Scripts/Javascript.js", + mocks.MockServerHost.libPath + ]); + + + host.vfs.watchFiles = false; + host.vfs.removeFile("/a/rootFolder/project/Scripts/TypeScript.ts"); + host.runQueuedTimeoutCallbacks(); + + const writeFileSpy1 = new Spy(writeFile); + host.writeFile = writeFileSpy1.value; + + host.vfs.writeFile("/a/rootFolder/project/Scripts/TypeScript.ts", `var zz30 = 100;`); + host.runQueuedTimeoutCallbacks(); + + checkProgramActualFiles(watch(), [ + "/a/rootFolder/project/Scripts/TypeScript.ts", + "/a/rootFolder/project/Scripts/Javascript.js", + mocks.MockServerHost.libPath + ]); + + writeFileSpy1.verify(_ => _("/a/rootFolder/project/Static/scripts/TypeScript.js", `var zz30 = 100;\n`, Arg.any()), Times.once()); + }); }); }); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index ea573726eb8..0ef61c1317d 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -401,9 +401,9 @@ namespace ts.projectSystem { } } - export function makeSessionRequest(command: string, args: T) { + export function makeSessionRequest(command: string, args: T, seq = 0) { const newRequest: protocol.Request = { - seq: 0, + seq, type: "request", command, arguments: args @@ -411,9 +411,29 @@ namespace ts.projectSystem { return newRequest; } - export function openFilesForSession(files: FileOrFolder[], session: server.Session) { + export function sendOpenRequest(session: server.Session, args: server.protocol.OpenRequestArgs, seq?: number) { + session.executeCommand(makeSessionRequest(CommandNames.Open, args, seq)); + } + + export function sendChangeRequest(session: server.Session, args: server.protocol.ChangeRequestArgs, seq?: number) { + session.executeCommand(makeSessionRequest(CommandNames.Change, args, seq)); + } + + export function sendCompileOnSaveAffectedFileListRequest(session: server.Session, args: server.protocol.FileRequestArgs, seq?: number) { + return session.executeCommand(makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, args, seq)).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; + } + + export function sendCompileOnSaveEmitFileRequest(session: server.Session, args: server.protocol.CompileOnSaveEmitFileRequestArgs, seq?: number) { + session.executeCommand(makeSessionRequest(CommandNames.CompileOnSaveEmitFile, args, seq)); + } + + export function sendCompilerOptionsDiagnosticsRequest(session: server.Session, args: server.protocol.CompilerOptionsDiagnosticsRequestArgs, seq?: number) { + return session.executeCommand(makeSessionRequest(CommandNames.CompilerOptionsDiagnosticsFull, args, seq)).response as server.protocol.DiagnosticWithLinePosition[]; + } + + export function openFilesForSession(files: (string | FileOrFolder | vfs.VirtualFile)[], session: server.Session) { for (const file of files) { - const request = makeSessionRequest(CommandNames.Open, { file: file.path }); + const request = makeSessionRequest(CommandNames.Open, { file: typeof file === "string" ? file : file.path }); session.executeCommand(request); } } diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index e321fb1c99d..2d9a86607fa 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -1,6 +1,9 @@ /// /// /// +/// +/// +/// namespace ts.projectSystem { import TI = server.typingsInstaller; @@ -37,10 +40,20 @@ namespace ts.projectSystem { } } - function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[] | string, typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { + function executeCommand(self: Installer, host: TestServerHost | mocks.MockServerHost, installedTypings: string[] | string, typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { self.addPostExecAction(installedTypings, success => { for (const file of typingFiles) { - host.ensureFileOrFolder(file); + if (host instanceof mocks.MockServerHost) { + if (typeof file.content === "string") { + host.vfs.addFile(file.path, file.content, { overwrite: true }); + } + else { + host.vfs.addDirectory(file.path); + } + } + else { + host.ensureFileOrFolder(file); + } } cb(success); }); @@ -62,35 +75,22 @@ namespace ts.projectSystem { describe("local module", () => { it("should not be picked up", () => { - const f1 = { - path: "/a/app.js", - content: "const c = require('./config');" - }; - const f2 = { - path: "/a/config.js", - content: "export let x = 1" - }; - const typesCache = "/cache"; - const typesConfig = { - path: typesCache + "/node_modules/@types/config/index.d.ts", - content: "export let y: number;" - }; - const config = { - path: "/a/jsconfig.json", - content: JSON.stringify({ - compilerOptions: { moduleResolution: "commonjs" }, - typeAcquisition: { enable: true } - }) - }; - const host = createServerHost([f1, f2, config, typesConfig]); - const installer = new (class extends Installer { + const host = new mocks.MockServerHost(); + const f1 = host.vfs.addFile("/a/app.js", `const c = require('./config');`); + const f2 = host.vfs.addFile("/a/config.js", `export let x = 1`); + const config = host.vfs.addFile("/a/jsconfig.json", `{ "typeAcquisition": { "enable": true }, "compilerOptions": { "moduleResolution": "commonjs } }`); + host.vfs.addFile("/cache/node_modules/@types/config/index.d.ts", `export let y: number;`); + + const installer = new class extends Installer { constructor() { - super(host, { typesRegistry: createTypesRegistry("config"), globalTypingsCacheLocation: typesCache }); + super(host, { typesRegistry: createTypesRegistry("config"), globalTypingsCacheLocation: "/cache" }); } + installWorker(_requestId: number, _args: string[], _cwd: string, _cb: TI.RequestCompletedAction) { assert(false, "should not be called"); } - })(); + }; + const service = createProjectService(host, { typingsInstaller: installer }); service.openClientFile(f1.path); service.checkNumberOfProjects({ configuredProjects: 1 }); @@ -101,46 +101,27 @@ namespace ts.projectSystem { describe("typingsInstaller", () => { it("configured projects (typings installed) 1", () => { - const file1 = { - path: "/a/b/app.js", - content: "" - }; - const tsconfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - allowJs: true - }, - typeAcquisition: { - enable: true - } - }) - }; - const packageJson = { - path: "/a/b/package.json", - content: JSON.stringify({ - name: "test", - dependencies: { - jquery: "^3.1.0" - } - }) - }; - const jquery = { path: "/a/data/node_modules/@types/jquery/index.d.ts", content: "declare const $: { x: number }" }; - const host = createServerHost([file1, tsconfig, packageJson]); - const installer = new (class extends Installer { + + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/app.js", ``); + const tsconfig = host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": { "allowJs": true }, "typeAcquisition": { "enable": true } }`); + host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "jquery": "^3.1.0" } }`); + + const installer = new class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jquery]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); projectService.openClientFile(file1.path); @@ -157,35 +138,26 @@ namespace ts.projectSystem { }); it("inferred project (typings installed)", () => { - const file1 = { - path: "/a/b/app.js", - content: "" - }; - const packageJson = { - path: "/a/b/package.json", - content: JSON.stringify({ - name: "test", - dependencies: { - jquery: "^3.1.0" - } - }) - }; - const jquery = { path: "/a/data/node_modules/@types/jquery/index.d.ts", content: "declare const $: { x: number }" }; - const host = createServerHost([file1, packageJson]); - const installer = new (class extends Installer { + + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/app.js", ``); + host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "jquery": "^3.1.0" } }`); + + const installer = new class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) { const installedTypings = ["@types/jquery"]; const typingFiles = [jquery]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); projectService.openClientFile(file1.path); @@ -201,19 +173,18 @@ namespace ts.projectSystem { }); it("external project - no type acquisition, no .d.ts/js files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const host = createServerHost([file1]); - const installer = new (class extends Installer { + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/app.ts", ``); + + const installer = new class extends Installer { constructor() { super(host); } + enqueueInstallTypingsRequest() { assert(false, "auto discovery should not be enabled"); } - })(); + }; const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -222,6 +193,7 @@ namespace ts.projectSystem { options: {}, rootFiles: [toExternalFile(file1.path)] }); + installer.checkPendingCommands(/*expectedCount*/ 0); // by default auto discovery will kick in if project contain only .js/.d.ts files // in this case project contain only ts files - no auto discovery @@ -229,19 +201,16 @@ namespace ts.projectSystem { }); it("external project - no auto in typing acquisition, no .d.ts/js files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const host = createServerHost([file1]); - const installer = new (class extends Installer { + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/app.ts", ``); + const installer = new class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); } enqueueInstallTypingsRequest() { assert(false, "auto discovery should not be enabled"); } - })(); + }; const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -258,17 +227,16 @@ namespace ts.projectSystem { }); it("external project - autoDiscovery = true, no .d.ts/js files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/app.ts", ``); + const jquery = { path: "/a/data/node_modules/@types/jquery/index.d.ts", content: "declare const $: { x: number }" }; - const host = createServerHost([file1]); + let enqueueIsCalled = false; - const installer: Installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); } @@ -281,7 +249,7 @@ namespace ts.projectSystem { const typingFiles = [jquery]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -304,18 +272,12 @@ namespace ts.projectSystem { // 1. react typings are installed for .jsx // 2. loose files names are matched against safe list for typings if // this is a JS project (only js, jsx, d.ts files are present) - const file1 = { - path: "/a/b/lodash.js", - content: "" - }; - const file2 = { - path: "/a/b/file2.jsx", - content: "" - }; - const file3 = { - path: "/a/b/file3.d.ts", - content: "" - }; + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/lodash.js", ``); + const file2 = host.vfs.addFile("/a/b/file2.jsx", ``); + const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``); + host.vfs.addFile(customTypesMap.path, customTypesMap.content); + const react = { path: "/a/data/node_modules/@types/react/index.d.ts", content: "declare const react: { x: number }" @@ -325,8 +287,7 @@ namespace ts.projectSystem { content: "declare const lodash: { x: number }" }; - const host = createServerHost([file1, file2, file3, customTypesMap]); - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("lodash", "react") }); } @@ -335,7 +296,7 @@ namespace ts.projectSystem { const typingFiles = [lodash, react]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -361,17 +322,11 @@ namespace ts.projectSystem { it("external project - no type acquisition, with js & ts files", () => { // Tests: // 1. No typings are included for JS projects when the project contains ts files - const file1 = { - path: "/a/b/jquery.js", - content: "" - }; - const file2 = { - path: "/a/b/file2.ts", - content: "" - }; + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/jquery.js", ``); + const file2 = host.vfs.addFile("/a/b/file2.ts", ``); - const host = createServerHost([file1, file2]); - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); } @@ -383,7 +338,7 @@ namespace ts.projectSystem { const typingFiles: FileOrFolder[] = []; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -409,27 +364,12 @@ namespace ts.projectSystem { // 1. Safelist matching, type acquisition includes/excludes and package.json typings are all acquired // 2. Types for safelist matches are not included when they also appear in the type acquisition exclude list // 3. Multiple includes and excludes are respected in type acquisition - const file1 = { - path: "/a/b/lodash.js", - content: "" - }; - const file2 = { - path: "/a/b/commander.js", - content: "" - }; - const file3 = { - path: "/a/b/file3.d.ts", - content: "" - }; - const packageJson = { - path: "/a/b/package.json", - content: JSON.stringify({ - name: "test", - dependencies: { - express: "^3.1.0" - } - }) - }; + const host = new mocks.MockServerHost({ safeList: true }); + const file1 = host.vfs.addFile("/a/b/lodash.js", ``); + const file2 = host.vfs.addFile("/a/b/commander.js", ``); + const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``); + host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "express": "^3.1.0" } }`); + host.vfs.addFile(customTypesMap.path, customTypesMap.content); const commander = { path: "/a/data/node_modules/@types/commander/index.d.ts", @@ -448,8 +388,7 @@ namespace ts.projectSystem { content: "declare const moment: { x: number }" }; - const host = createServerHost([file1, file2, file3, packageJson, customTypesMap]); - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery", "commander", "moment", "express") }); } @@ -458,7 +397,7 @@ namespace ts.projectSystem { const typingFiles = [commander, express, jquery, moment]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -482,27 +421,12 @@ namespace ts.projectSystem { }); it("Throttle - delayed typings to install", () => { - const lodashJs = { - path: "/a/b/lodash.js", - content: "" - }; - const commanderJs = { - path: "/a/b/commander.js", - content: "" - }; - const file3 = { - path: "/a/b/file3.d.ts", - content: "" - }; - const packageJson = { - path: "/a/b/package.json", - content: JSON.stringify({ - name: "test", - dependencies: { - express: "^3.1.0" - } - }) - }; + const host = new mocks.MockServerHost({ safeList: true }); + const lodashJs = host.vfs.addFile("/a/b/lodash.js", ``); + const commanderJs = host.vfs.addFile("/a/b/commander.js", ``); + const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``); + host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "express": "^3.1.0" } }`); + host.vfs.addFile(customTypesMap.path, customTypesMap.content); const commander = { path: "/a/data/node_modules/@types/commander/index.d.ts", @@ -526,8 +450,7 @@ namespace ts.projectSystem { }; const typingFiles = [commander, express, jquery, moment, lodash]; - const host = createServerHost([lodashJs, commanderJs, file3, packageJson, customTypesMap]); - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { throttleLimit: 3, typesRegistry: createTypesRegistry("commander", "express", "jquery", "moment", "lodash") }); } @@ -535,7 +458,7 @@ namespace ts.projectSystem { const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment", "@types/lodash"]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectFileName = "/a/app/test.csproj"; const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -561,18 +484,11 @@ namespace ts.projectSystem { }); it("Throttle - delayed run install requests", () => { - const lodashJs = { - path: "/a/b/lodash.js", - content: "" - }; - const commanderJs = { - path: "/a/b/commander.js", - content: "" - }; - const file3 = { - path: "/a/b/file3.d.ts", - content: "" - }; + const host = new mocks.MockServerHost({ safeList: true }); + const lodashJs = host.vfs.addFile("/a/b/lodash.js", ``); + const commanderJs = host.vfs.addFile("/a/b/commander.js", ``); + const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``); + host.vfs.addFile(customTypesMap.path, customTypesMap.content); const commander = { path: "/a/data/node_modules/@types/commander/index.d.ts", @@ -605,8 +521,7 @@ namespace ts.projectSystem { typings: typingsName("gulp") }; - const host = createServerHost([lodashJs, commanderJs, file3, customTypesMap]); - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { throttleLimit: 1, typesRegistry: createTypesRegistry("commander", "jquery", "lodash", "cordova", "gulp", "grunt") }); } @@ -620,7 +535,7 @@ namespace ts.projectSystem { } executeCommand(this, host, typingFiles.map(f => f.typings), typingFiles, cb); } - })(); + }; // Create project #1 with 4 typings const projectService = createProjectService(host, { typingsInstaller: installer }); @@ -658,39 +573,25 @@ namespace ts.projectSystem { assert.equal(installer.pendingRunRequests.length, 0, "expected no throttled requests"); installer.executePendingCommands(); - host.checkTimeoutQueueLengthAndRun(3); // for 2 projects and 1 refreshing inferred project + host.checkTimeoutQueueLengthAndRun(3); checkProjectActualFiles(p1, [lodashJs.path, commanderJs.path, file3.path, commander.path, jquery.path, lodash.path, cordova.path]); checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path]); }); it("configured projects discover from node_modules", () => { - const app = { - path: "/app.js", - content: "" - }; - const jsconfig = { - path: "/jsconfig.json", - content: JSON.stringify({}) - }; - const jquery = { - path: "/node_modules/jquery/index.js", - content: "" - }; - const jqueryPackage = { - path: "/node_modules/jquery/package.json", - content: JSON.stringify({ name: "jquery" }) - }; - // Should not search deeply in node_modules. - const nestedPackage = { - path: "/node_modules/jquery/nested/package.json", - content: JSON.stringify({ name: "nested" }), - }; const jqueryDTS = { path: "/tmp/node_modules/@types/jquery/index.d.ts", content: "" }; - const host = createServerHost([app, jsconfig, jquery, jqueryPackage, nestedPackage]); - const installer = new (class extends Installer { + const host = new mocks.MockServerHost({ safeList: true }); + const app = host.vfs.addFile("/app.js", ``); + const jsconfig = host.vfs.addFile("/jsconfig.json", `{}`); + host.vfs.addFile("/node_modules/jquery/index.js", ``); + host.vfs.addFile("/node_modules/jquery/package.json", `{ "name": "jquery" }`); + // Should not search deeply in node_modules. + host.vfs.addFile("/node_modules/jquery/nested/package.json", `{ "name": "nested" }`); + + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery", "nested") }); } @@ -700,7 +601,7 @@ namespace ts.projectSystem { const typingFiles = [jqueryDTS]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); projectService.openClientFile(app.path); @@ -717,28 +618,18 @@ namespace ts.projectSystem { }); it("configured projects discover from bower_components", () => { - const app = { - path: "/app.js", - content: "" - }; - const jsconfig = { - path: "/jsconfig.json", - content: JSON.stringify({}) - }; - const jquery = { - path: "/bower_components/jquery/index.js", - content: "" - }; - const jqueryPackage = { - path: "/bower_components/jquery/package.json", - content: JSON.stringify({ name: "jquery" }) - }; + const host = new mocks.MockServerHost({ safeList: true }); + const app = host.vfs.addFile("/app.js", ``); + const jsconfig = host.vfs.addFile("/jsconfig.json", `{}`); + host.vfs.addFile("/bower_components/jquery/index.js", ``); + host.vfs.addFile("/bower_components/jquery/package.json", `{ "name": "jquery" }`); + const jqueryDTS = { path: "/tmp/node_modules/@types/jquery/index.d.ts", content: "" }; - const host = createServerHost([app, jsconfig, jquery, jqueryPackage]); - const installer = new (class extends Installer { + + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") }); } @@ -747,7 +638,7 @@ namespace ts.projectSystem { const typingFiles = [jqueryDTS]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); projectService.openClientFile(app.path); @@ -755,7 +646,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [app.path, jsconfig.path]); - checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", libFile.path]); + checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", "/.ts/lib.d.ts"]); installer.installAll(/*expectedCount*/ 1); @@ -765,28 +656,17 @@ namespace ts.projectSystem { }); it("configured projects discover from bower.json", () => { - const app = { - path: "/app.js", - content: "" - }; - const jsconfig = { - path: "/jsconfig.json", - content: JSON.stringify({}) - }; - const bowerJson = { - path: "/bower.json", - content: JSON.stringify({ - dependencies: { - jquery: "^3.1.0" - } - }) - }; + const host = new mocks.MockServerHost({ safeList: true }); + const app = host.vfs.addFile("/app.js", ``); + const jsconfig = host.vfs.addFile("/jsconfig.json", `{}`); + host.vfs.addFile("/bower.json", `{ "dependencies": { "jquery": "^3.1.0" } }`); + const jqueryDTS = { path: "/tmp/node_modules/@types/jquery/index.d.ts", content: "" }; - const host = createServerHost([app, jsconfig, bowerJson]); - const installer = new (class extends Installer { + + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") }); } @@ -795,7 +675,7 @@ namespace ts.projectSystem { const typingFiles = [jqueryDTS]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer }); projectService.openClientFile(app.path); @@ -812,25 +692,17 @@ namespace ts.projectSystem { }); it("Malformed package.json should be watched", () => { - const f = { - path: "/a/b/app.js", - content: "var x = 1" - }; - const brokenPackageJson = { - path: "/a/b/package.json", - content: `{ "dependencies": { "co } }` - }; - const fixedPackageJson = { - path: brokenPackageJson.path, - content: `{ "dependencies": { "commander": "0.0.2" } }` - }; + const host = new mocks.MockServerHost({ safeList: true }); + const f = host.vfs.addFile("/a/b/app.js", `var x = 1`); + host.vfs.addFile("/a/b/package.json", `{ "dependencies": { "co } }`); + const cachePath = "/a/cache/"; const commander = { path: cachePath + "node_modules/@types/commander/index.d.ts", content: "export let x: number" }; - const host = createServerHost([f, brokenPackageJson]); - const installer = new (class extends Installer { + + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } @@ -839,28 +711,32 @@ namespace ts.projectSystem { const typingFiles = [commander]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; + const service = createProjectService(host, { typingsInstaller: installer }); service.openClientFile(f.path); installer.checkPendingCommands(/*expectedCount*/ 0); - host.reloadFS([f, fixedPackageJson]); + host.vfs.writeFile("/a/b/package.json", `{ "dependencies": { "commander": "0.0.2" } }`); + host.checkTimeoutQueueLengthAndRun(2); // To refresh the project and refresh inferred projects + // expected install request installer.installAll(/*expectedCount*/ 1); + host.checkTimeoutQueueLengthAndRun(2); + service.checkNumberOfProjects({ inferredProjects: 1 }); checkProjectActualFiles(service.inferredProjects[0], [f.path, commander.path]); }); it("should install typings for unresolved imports", () => { - const file = { - path: "/a/b/app.js", - content: ` - import * as fs from "fs"; - import * as commander from "commander";` - }; + const host = new mocks.MockServerHost({ safeList: true }); + const file = host.vfs.addFile("/a/b/app.js", + `import * as fs from "fs";\n` + + `import * as commander from "commander";`); + const cachePath = "/a/cache"; const node = { path: cachePath + "/node_modules/@types/node/index.d.ts", @@ -870,8 +746,8 @@ namespace ts.projectSystem { path: cachePath + "/node_modules/@types/commander/index.d.ts", content: "export let y: string" }; - const host = createServerHost([file]); - const installer = new (class extends Installer { + + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("node", "commander") }); } @@ -880,7 +756,8 @@ namespace ts.projectSystem { const typingFiles = [node, commander]; executeCommand(this, host, installedTypings, typingFiles, cb); } - })(); + }; + const service = createProjectService(host, { typingsInstaller: installer }); service.openClientFile(file.path); @@ -896,28 +773,25 @@ namespace ts.projectSystem { }); it("should pick typing names from non-relative unresolved imports", () => { - const f1 = { - path: "/a/b/app.js", - content: ` - import * as a from "foo/a/a"; - import * as b from "foo/a/b"; - import * as c from "foo/a/c"; - import * as d from "@bar/router/"; - import * as e from "@bar/common/shared"; - import * as e from "@bar/common/apps"; - import * as f from "./lib" - ` - }; + const host = new mocks.MockServerHost({ safeList: true }); + const f1 = host.vfs.addFile("/a/b/app.js", + `import * as a from "foo/a/a";\n` + + `import * as b from "foo/a/b";\n` + + `import * as c from "foo/a/c";\n` + + `import * as d from "@bar/router/";\n` + + `import * as e from "@bar/common/shared";\n` + + `import * as e from "@bar/common/apps";\n` + + `import * as f from "./lib"`); - const host = createServerHost([f1]); - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("foo") }); } installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) { executeCommand(this, host, ["foo"], [], cb); } - })(); + }; + const projectService = createProjectService(host, { typingsInstaller: installer }); projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ inferredProjects: 1 }); @@ -934,7 +808,7 @@ namespace ts.projectSystem { }); it("cached unresolved typings are not recomputed if program structure did not change", () => { - const host = createServerHost([]); + const host = new mocks.MockServerHost({ safeList: true }); const session = createSession(host); const f = { path: "/a/app.js", @@ -1005,28 +879,20 @@ namespace ts.projectSystem { describe("Invalid package names", () => { it("should not be installed", () => { - const f1 = { - path: "/a/b/app.js", - content: "let x = 1" - }; - const packageJson = { - path: "/a/b/package.json", - content: JSON.stringify({ - dependencies: { - "; say ‘Hello from TypeScript!’ #": "0.0.x" - } - }) - }; + const host = new mocks.MockServerHost({ safeList: true }); + const f1 = host.vfs.addFile("/a/b/app.js", `let x = 1`); + host.vfs.addFile("/a/b/package.json", `{ "dependencies": { "; say ‘Hello from TypeScript!’ #": "0.0.x" } }`); + const messages: string[] = []; - const host = createServerHost([f1, packageJson]); - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) }); } installWorker(_requestId: number, _args: string[], _cwd: string, _cb: TI.RequestCompletedAction) { assert(false, "runCommand should not be invoked"); } - })(); + }; + const projectService = createProjectService(host, { typingsInstaller: installer }); projectService.openClientFile(f1.path); @@ -1039,22 +905,14 @@ namespace ts.projectSystem { const emptySafeList = emptyMap; it("should use mappings from safe list", () => { - const app = { - path: "/a/b/app.js", - content: "" - }; - const jquery = { - path: "/a/b/jquery.js", - content: "" - }; - const chroma = { - path: "/a/b/chroma.min.js", - content: "" - }; + const host = new mocks.MockServerHost({ safeList: true }); + const app = host.vfs.addFile("/a/b/app.js", ``); + const jquery = host.vfs.addFile("/a/b/jquery.js", ``); + const chroma = host.vfs.addFile("/a/b/chroma.min.js", ``); const safeList = createMapFromTemplate({ jquery: "jquery", chroma: "chroma-js" }); - const host = createServerHost([app, jquery, chroma]); + // const host = createServerHost([app, jquery, chroma]); const logger = trackingLogger(); const result = JsTyping.discoverTypings(host, logger.log, [app.path, jquery.path, chroma.path], getDirectoryPath(app.path), safeList, emptyMap, { enable: true }, emptyArray); const finish = logger.finish(); @@ -1067,11 +925,9 @@ namespace ts.projectSystem { }); it("should return node for core modules", () => { - const f = { - path: "/a/b/app.js", - content: "" - }; - const host = createServerHost([f]); + const host = new mocks.MockServerHost({ safeList: true }); + const f = host.vfs.addFile("/a/b/app.js", ``); + const cache = createMap(); for (const name of JsTyping.nodeCoreModuleList) { @@ -1086,15 +942,10 @@ namespace ts.projectSystem { }); it("should use cached locations", () => { - const f = { - path: "/a/b/app.js", - content: "" - }; - const node = { - path: "/a/b/node.d.ts", - content: "" - }; - const host = createServerHost([f, node]); + const host = new mocks.MockServerHost({ safeList: true }); + const f = host.vfs.addFile("/a/b/app.js", ``); + const node = host.vfs.addFile("/a/b/node.d.ts", ``); + const cache = createMapFromTemplate({ node: node.path }); const logger = trackingLogger(); const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(f.path), emptySafeList, cache, { enable: true }, ["fs", "bar"]); @@ -1107,19 +958,11 @@ namespace ts.projectSystem { }); it("should search only 2 levels deep", () => { - const app = { - path: "/app.js", - content: "", - }; - const a = { - path: "/node_modules/a/package.json", - content: JSON.stringify({ name: "a" }), - }; - const b = { - path: "/node_modules/a/b/package.json", - content: JSON.stringify({ name: "b" }), - }; - const host = createServerHost([app, a, b]); + const host = new mocks.MockServerHost({ safeList: true }); + const app = host.vfs.addFile("/app.js"); + host.vfs.addFile("/node_modules/a/package.json", `{ "name": "a" }`); + host.vfs.addFile("/node_modules/a/b/package.json", `{ "name": "b" }`); + const cache = createMap(); const logger = trackingLogger(); const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(app.path), emptySafeList, cache, { enable: true }, /*unresolvedImports*/ []); @@ -1139,22 +982,18 @@ namespace ts.projectSystem { describe("telemetry events", () => { it("should be received", () => { - const f1 = { - path: "/a/app.js", - content: "" - }; - const packageFile = { - path: "/a/package.json", - content: JSON.stringify({ dependencies: { commander: "1.0.0" } }) - }; + const host = new mocks.MockServerHost({ safeList: true }); + const f1 = host.vfs.addFile("/a/app.js", ``); + host.vfs.addFile("/a/package.json", `{ "dependencies": { "commander": "1.0.0" } }`); + const cachePath = "/a/cache/"; const commander = { path: cachePath + "node_modules/@types/commander/index.d.ts", content: "export let x: number" }; - const host = createServerHost([f1, packageFile]); + let seenTelemetryEvent = false; - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } @@ -1174,7 +1013,8 @@ namespace ts.projectSystem { } super.sendResponse(response); } - })(); + }; + const projectService = createProjectService(host, { typingsInstaller: installer }); projectService.openClientFile(f1.path); @@ -1189,23 +1029,19 @@ namespace ts.projectSystem { describe("progress notifications", () => { it("should be sent for success", () => { - const f1 = { - path: "/a/app.js", - content: "" - }; - const packageFile = { - path: "/a/package.json", - content: JSON.stringify({ dependencies: { commander: "1.0.0" } }) - }; + const host = new mocks.MockServerHost({ safeList: true }); + const f1 = host.vfs.addFile("/a/app.js", ``); + host.vfs.addFile("/a/package.json", `{ "dependencies": { "commander": "1.0.0" } }`); + const cachePath = "/a/cache/"; const commander = { path: cachePath + "node_modules/@types/commander/index.d.ts", content: "export let x: number" }; - const host = createServerHost([f1, packageFile]); + let beginEvent: server.BeginInstallTypes; let endEvent: server.EndInstallTypes; - const installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } @@ -1225,7 +1061,8 @@ namespace ts.projectSystem { } super.sendResponse(response); } - })(); + }; + const projectService = createProjectService(host, { typingsInstaller: installer }); projectService.openClientFile(f1.path); @@ -1241,19 +1078,15 @@ namespace ts.projectSystem { }); it("should be sent for error", () => { - const f1 = { - path: "/a/app.js", - content: "" - }; - const packageFile = { - path: "/a/package.json", - content: JSON.stringify({ dependencies: { commander: "1.0.0" } }) - }; + // const host = createServerHost([f1, packageFile]); + const host = new mocks.MockServerHost({ safeList: true }); + const f1 = host.vfs.addFile("/a/app.js", ``); + host.vfs.addFile("/a/package.json", `{ "dependencies": { "commander": "1.0.0" } }`); + const cachePath = "/a/cache/"; - const host = createServerHost([f1, packageFile]); let beginEvent: server.BeginInstallTypes; let endEvent: server.EndInstallTypes; - const installer: Installer = new (class extends Installer { + const installer = new class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } @@ -1271,7 +1104,8 @@ namespace ts.projectSystem { } super.sendResponse(response); } - })(); + }; + const projectService = createProjectService(host, { typingsInstaller: installer }); projectService.openClientFile(f1.path); diff --git a/src/harness/utils.ts b/src/harness/utils.ts index 4d0d43d7fba..5cc43c6ed38 100644 --- a/src/harness/utils.ts +++ b/src/harness/utils.ts @@ -33,4 +33,26 @@ namespace utils { export function removeTestPathPrefixes(text: string) { return text !== undefined ? text.replace(testPathPrefixRegExp, "") : undefined; } + + /** + * SameValueZero (from ECMAScript spec), which has stricter equality sematics than "==" or "===". + */ + export function is(x: any, y: any) { + return (x === y) ? (x !== 0 || 1 / x === 1 / y) : (x !== x && y !== y); + } + + export interface Theory { + title: string; + args: any[]; + } + + export function theory(name: string, data: (Theory | any[])[], callback: (...args: any[]) => any) { + describe(name, () => { + for (const theory of data) { + const title = Array.isArray(theory) ? theory.toString() : theory.title; + const args = Array.isArray(theory) ? theory : theory.args; + it(title, () => callback(...args)); + } + }); + } } \ No newline at end of file diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 92f34f6b832..aa2cc0e6e10 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -88,6 +88,7 @@ namespace vfs { interface DirectoryWatcherEntryArray extends Array { recursiveCount?: number; + nonRecursiveCount?: number; } interface DirectoryWatcherEntry { @@ -118,6 +119,9 @@ namespace vfs { } export class VirtualFileSystem extends VirtualFileSystemObject { + public watchFiles = true; + public watchDirectories = true; + private static _builtLocal: VirtualFileSystem | undefined; private static _builtLocalCI: VirtualFileSystem | undefined; private static _builtLocalCS: VirtualFileSystem | undefined; @@ -128,7 +132,10 @@ namespace vfs { private _currentDirectoryStack: string[] | undefined; private _shadowRoot: VirtualFileSystem | undefined; private _watchedFiles: core.KeyedCollection | undefined; + private _watchedFilesSet: core.SortedSet | undefined; private _watchedDirectories: core.KeyedCollection | undefined; + private _watchedRecursiveDirectoriesSet: core.SortedSet | undefined; + private _watchedNonRecursiveDirectoriesSet: core.SortedSet | undefined; private _stringComparer: ts.Comparer | undefined; private _pathComparer: ts.Comparer | undefined; private _metadata: core.Metadata; @@ -153,6 +160,18 @@ namespace vfs { : vpath.compareCaseInsensitive); } + public get watchedFiles(): ReadonlySet { + return this.watchedFilesSetPrivate; + } + + public get watchedRecursiveDirectories(): ReadonlySet { + return this.watchedRecursiveDirectoriesSetPrivate; + } + + public get watchedNonRecursiveDirectories(): ReadonlySet { + return this.watchedNonRecursiveDirectoriesSetPrivate; + } + /** * Gets the file system shadowed by this instance. */ @@ -181,6 +200,26 @@ namespace vfs { return this._currentDirectory; } + private get watchedFilesPrivate() { + return this._watchedFiles || (this._watchedFiles = new core.KeyedCollection(this.pathComparer)); + } + + private get watchedFilesSetPrivate() { + return this._watchedFilesSet || (this._watchedFilesSet = new core.SortedSet(this.pathComparer)); + } + + private get watchedDirectoriesPrivate() { + return this._watchedDirectories || (this._watchedDirectories = new core.KeyedCollection(this.pathComparer)) + } + + private get watchedRecursiveDirectoriesSetPrivate() { + return this._watchedRecursiveDirectoriesSet || (this._watchedRecursiveDirectoriesSet = new core.SortedSet(this.pathComparer)); + } + + private get watchedNonRecursiveDirectoriesSetPrivate() { + return this._watchedNonRecursiveDirectoriesSet || (this._watchedNonRecursiveDirectoriesSet = new core.SortedSet(this.pathComparer)); + } + private get root(): VirtualRoot { if (this._root === undefined) { if (this._shadowRoot) { @@ -361,8 +400,13 @@ namespace vfs { */ public writeFile(path: string, content: string): void { path = vpath.resolve(this.currentDirectory, path); - const file = this.getFile(path) || this.addFile(path); - if (file) file.writeContent(content); + const file = this.getFile(path); + if (file) { + file.writeContent(content); + } + else { + this.addFile(path, content); + } } /** @@ -508,25 +552,24 @@ namespace vfs { * Watch a path for changes to a file. */ public watchFile(path: string, watcher: (path: string, change: FileSystemChange) => void): ts.FileWatcher { - if (!this._watchedFiles) { - const pathComparer = this.useCaseSensitiveFileNames ? vpath.compareCaseSensitive : vpath.compareCaseInsensitive; - this._watchedFiles = new core.KeyedCollection(pathComparer); - } - path = vpath.resolve(this.currentDirectory, path); - let watchers = this._watchedFiles.get(path); - if (!watchers) this._watchedFiles.set(path, watchers = []); + let watchers = this.watchedFilesPrivate.get(path); + if (!watchers) { + this.watchedFilesPrivate.set(path, watchers = []); + this.watchedFilesSetPrivate.add(path); + } const entry: FileWatcherEntry = { watcher }; watchers.push(entry); return { close: () => { - const watchers = this._watchedFiles.get(path); + const watchers = this.watchedFilesPrivate.get(path); if (watchers) { ts.orderedRemoveItem(watchers, entry); if (watchers.length === 0) { - this._watchedFiles.delete(path); + this.watchedFilesPrivate.delete(path); + this.watchedFilesSetPrivate.delete(path); } } } @@ -543,27 +586,48 @@ namespace vfs { } path = vpath.resolve(this.currentDirectory, path); - let watchers = this._watchedDirectories.get(path); + let watchers = this.watchedDirectoriesPrivate.get(path); if (!watchers) { watchers = []; watchers.recursiveCount = 0; - this._watchedDirectories.set(path, watchers); + watchers.nonRecursiveCount = 0; + this.watchedDirectoriesPrivate.set(path, watchers); } const entry: DirectoryWatcherEntry = { watcher, recursive }; watchers.push(entry); - if (recursive) watchers.recursiveCount++; + if (recursive) { + if (watchers.recursiveCount === 0) { + this.watchedRecursiveDirectoriesSetPrivate.add(path); + } + watchers.recursiveCount++; + } + else { + if (watchers.nonRecursiveCount === 0) { + this.watchedNonRecursiveDirectoriesSetPrivate.add(path); + } + watchers.nonRecursiveCount++; + } return { close: () => { - const watchers = this._watchedDirectories.get(path); + const watchers = this.watchedDirectoriesPrivate.get(path); if (watchers) { ts.orderedRemoveItem(watchers, entry); - if (watchers.length === 0) { - this._watchedDirectories.delete(path); - } - else if (entry.recursive) { + if (entry.recursive) { watchers.recursiveCount--; + if (watchers.recursiveCount === 0) { + this.watchedRecursiveDirectoriesSetPrivate.delete(path); + } + } + else { + watchers.nonRecursiveCount--; + if (watchers.nonRecursiveCount === 0) { + this.watchedNonRecursiveDirectoriesSetPrivate.delete(path); + } + } + if (watchers.length === 0) { + this.watchedDirectoriesPrivate.delete(path); } } } @@ -603,26 +667,31 @@ namespace vfs { } private onRootFileSystemChange(path: string, change: FileSystemChange) { - const fileWatchers = this._watchedFiles && this._watchedFiles.get(path); - if (fileWatchers) { - for (const { watcher } of fileWatchers) { - watcher(path, change); + if (this.watchFiles) { + const fileWatchers = this._watchedFiles && this._watchedFiles.get(path); + if (fileWatchers) { + for (const { watcher } of fileWatchers) { + watcher(path, change); + } } } - if (this._watchedDirectories && (change === "added" || change === "removed")) { - const ignoreCase = !this.useCaseSensitiveFileNames; - const dirname = vpath.dirname(path); - this._watchedDirectories.forEach((watchers, path) => { - const exactMatch = vpath.equals(dirname, path, ignoreCase); - if (exactMatch || (watchers.recursiveCount > 0 && vpath.beneath(dirname, path, ignoreCase))) { - for (const { recursive, watcher } of watchers) { - if (exactMatch || !recursive) { - watcher(path); + if (this.watchDirectories) { + if (this._watchedDirectories && (change === "added" || change === "removed")) { + const ignoreCase = !this.useCaseSensitiveFileNames; + const dirname = vpath.dirname(path); + this._watchedDirectories.forEach((watchers, watchedPath) => { + const nonRecursiveMatch = watchers.nonRecursiveCount > 0 && vpath.equals(watchedPath, dirname, ignoreCase); + const recursiveMatch = watchers.recursiveCount > 0 && vpath.beneath(watchedPath, dirname, ignoreCase); + if (nonRecursiveMatch || recursiveMatch) { + for (const { recursive, watcher } of watchers) { + if (recursive ? recursiveMatch : nonRecursiveMatch) { + watcher(path); + } } } - } - }); + }); + } } } } @@ -766,10 +835,16 @@ namespace vfs { protected static _setNameUnsafe(entry: VirtualFileSystemEntry, name: string) { entry._name = name; + entry.invalidate(); } protected static _setParentUnsafe(entry: VirtualFileSystemEntry, parent: VirtualDirectory) { entry._parent = parent; + entry.invalidate(); + } + + protected static _invalidate(entry: VirtualFileSystemEntry) { + entry.invalidate(); } protected shadowPreamble(parent: VirtualDirectory): void { @@ -812,6 +887,10 @@ namespace vfs { this._mtimeMS = Date.now(); } } + + protected invalidate() { + this._path = undefined; + } } export interface VirtualDirectory { @@ -879,6 +958,10 @@ namespace vfs { return this._shadowRoot; } + protected get hasOwnEntries() { + return this._entries !== undefined; + } + /** * Gets the child entries in this directory for the provided options. */ @@ -1253,6 +1336,19 @@ namespace vfs { return true; } + protected invalidate() { + super.invalidate(); + this.invalidateEntries(); + } + + protected invalidateEntries() { + if (this.hasOwnEntries) { + for (const entry of Array.from(this.getOwnEntries().values())) { + VirtualFileSystemEntry._invalidate(entry); + } + } + } + private parsePath(path: string) { return vpath.parse(vpath.normalize(path)); } @@ -1434,6 +1530,10 @@ namespace vfs { return this._allViews; } + protected invalidateEntries() { + this.invalidateTarget(); + } + private getView(entry: VirtualFile): VirtualFileView; private getView(entry: VirtualDirectory): VirtualDirectoryView; private getView(entry: VirtualEntry): VirtualEntryView; @@ -1788,6 +1888,11 @@ namespace vfs { return super.getStatsCore(S_IFLNK, this.targetPath.length); } + protected invalidate() { + super.invalidate(); + this.invalidateTarget(); + } + private resolveTarget() { if (!this._target) { const entry = findTarget(this.fileSystem, this.targetPath); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index c1d99edb8ed..b4b8a8141f0 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -5,7 +5,7 @@ namespace ts.TestFSWithWatch { export const libFile: FileOrFolder = { - path: "/a/lib/lib.d.ts", + path: "/.ts/lib.d.ts", content: `/// interface Boolean {} interface Function {} @@ -41,10 +41,24 @@ interface Array {}` useWindowsStylePaths?: boolean; } - export function createWatchedSystem(fileOrFolderList: ReadonlyArray, params?: TestServerHostCreationParameters): TestServerHost { - if (!params) { - params = {}; - } + export function createWatchedSystem(fileOrFolderList: ReadonlyArray, params: TestServerHostCreationParameters = {}) { + // const host = new mocks.MockServerHost({ + // vfs: { + // currentDirectory: params.currentDirectory, + // useCaseSensitiveFileNames: params.useCaseSensitiveFileNames + // }, + // executingFilePath: params.executingFilePath, + // newLine: params.newLine as "\r\n" | "\n" + // }); + // for (const entry of fileOrFolderList) { + // if (typeof entry.content === "string") { + // host.vfs.addFile(entry.path, entry.content); + // } + // else { + // host.vfs.addDirectory(entry.path); + // } + // } + // if (params.useWindowsStylePaths) throw new Error("Not supported"); const host = new TestServerHost(/*withSafelist*/ false, params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, params.executingFilePath || getExecutingFilePathFromLibFile(), @@ -153,11 +167,25 @@ interface Array {}` } } - export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { + function checkSortedSet(set: ReadonlySet, values: ReadonlyArray) { + assert.strictEqual(set.size, values.length, `Actual: ${Array.from(set)}, expected: ${values}.`); + for (const value of values) { + assert.isTrue(set.has(value)); + } + } + + export function checkWatchedFiles(host: TestServerHost | mocks.MockServerHost, expectedFiles: string[]) { + if (host instanceof mocks.MockServerHost) { + return checkSortedSet(host.vfs.watchedFiles, expectedFiles); + } + checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); } - export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) { + export function checkWatchedDirectories(host: TestServerHost | mocks.MockServerHost, expectedDirectories: string[], recursive = false) { + if (host instanceof mocks.MockServerHost) { + return checkSortedSet(recursive ? host.vfs.watchedRecursiveDirectories : host.vfs.watchedNonRecursiveDirectories, expectedDirectories); + } checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); } @@ -244,6 +272,16 @@ interface Array {}` ignoreWatchInvokedWithTriggerAsFileCreate: boolean; } + function createFileMatcher(host: TestServerHost, path: string) { + path = host.getCanonicalFileName(host.getHostSpecificPath(path)); + return (file: FileOrFolder) => host.getCanonicalFileName(host.getHostSpecificPath(file.path)) === path && isString(file.content); + } + + function createFolderMatcher(host: TestServerHost, path: string) { + path = host.getCanonicalFileName(host.getHostSpecificPath(path)); + return (file: FileOrFolder) => host.getCanonicalFileName(host.getHostSpecificPath(file.path)) === path && !isString(file.content); + } + export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost { args: string[] = []; @@ -260,6 +298,63 @@ interface Array {}` readonly watchedFiles = createMultiMap(); private readonly executingFilePath: string; private readonly currentDirectory: string; + public files: FileOrFolder[] = []; + + // temporary vfs shim + public vfs = { + addFile: (path: string, content: string, options?: { overwrite?: boolean }) => { + let file = this.files.find(createFileMatcher(this, path)); + if (file) { + if (!(options && options.overwrite)) return undefined; + file.content = content; + } + else { + file = { path, content }; + this.files.push(file); + } + this.reloadFS(this.files); + return file; + }, + writeFile: (path: string, content: string) => { + let file = this.files.find(createFileMatcher(this, path)); + if (file) { + file.content = content; + } + else { + file = { path, content }; + this.files.push(file); + } + this.reloadFS(this.files); + }, + getFile: (path: string) => { + return this.files.find(createFileMatcher(this, path)); + }, + removeFile: (path: string) => { + const index = this.files.findIndex(createFileMatcher(this, path)); + if (index >= 0) { + ts.orderedRemoveItemAt(this.files, index); + this.reloadFS(this.files); + } + }, + renameFile: (oldpath: string, newpath: string) => { + const oldItem = this.vfs.getFile(oldpath); + if (oldItem) { + const newIndex = this.files.findIndex(createFileMatcher(this, newpath)); + if (newIndex >= 0) ts.orderedRemoveItemAt(this.files, newIndex); + oldItem.path = newpath; + this.reloadFS(this.files); + } + }, + addDirectory: (path: string) => { + let file = this.files.find(createFolderMatcher(this, path)); + if (!file) { + file = { path }; + this.files.push(file); + this.reloadFS(this.files); + } + return file; + }, + }; constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); @@ -291,7 +386,9 @@ interface Array {}` reloadFS(fileOrFolderList: ReadonlyArray, options?: Partial) { const mapNewLeaves = createMap(); const isNewFs = this.fs.size === 0; - fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []); + if (this.files !== fileOrFolderList) { + this.files = fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []); + } const filesOrFoldersToLoad: ReadonlyArray = !this.useWindowsStylePath ? fileOrFolderList : fileOrFolderList.map(f => { const result = clone(f);