diff --git a/Gulpfile.ts b/Gulpfile.ts index dc2766e3ade..17d474126f2 100644 --- a/Gulpfile.ts +++ b/Gulpfile.ts @@ -625,21 +625,19 @@ function cleanPrivatePackage(packageName: string) { return del([`scripts/${packageName}/dist`]); } -gulp.task("typemock", () => compilePrivatePackage("typemock")); gulp.task("vfs-core", () => compilePrivatePackage("vfs-core")); gulp.task("vfs-errors", () => compilePrivatePackage("vfs-errors")); gulp.task("vfs-path", ["vfs-core", "vfs-errors"], () => compilePrivatePackage("vfs-path")); -gulp.task("vfs", ["vfs-core", "vfs-errors", "vfs-path", "typemock"], () => compilePrivatePackage("vfs")); +gulp.task("vfs", ["vfs-core", "vfs-errors", "vfs-path"], () => compilePrivatePackage("vfs")); gulp.task("harness-core", ["vfs-core"], () => compilePrivatePackage("harness-core")); -gulp.task("private-packages", ["typemock", "vfs", "harness-core"]); +gulp.task("private-packages", ["vfs", "harness-core"]); -gulp.task("clean:typemock", () => cleanPrivatePackage("typemock")); gulp.task("clean:vfs-core", () => cleanPrivatePackage("vfs-core")); gulp.task("clean:vfs-errors", () => cleanPrivatePackage("vfs-errors")); gulp.task("clean:vfs-path", ["clean:vfs-core", "clean:vfs-errors"], () => cleanPrivatePackage("vfs-path")); -gulp.task("clean:vfs", ["clean:vfs-core", "clean:vfs-errors", "clean:vfs-path", "clean:typemock"], () => cleanPrivatePackage("vfs")); +gulp.task("clean:vfs", ["clean:vfs-core", "clean:vfs-errors", "clean:vfs-path"], () => cleanPrivatePackage("vfs")); gulp.task("clean:harness-core", ["clean:vfs-core"], () => cleanPrivatePackage("harness-core")); -gulp.task("clean:private-packages", ["clean:typemock", "clean:vfs", "clean:harness-core"]); +gulp.task("clean:private-packages", ["clean:vfs", "clean:harness-core"]); // Task to build the tests infrastructure using the built compiler const run = path.join(builtLocalDirectory, "run.js"); @@ -1161,5 +1159,5 @@ gulp.task("lint", "Runs tslint on the compiler sources. Optional arguments are: gulp.task("default", "Runs 'local'", ["local"]); gulp.task("watch", "Watches the src/ directory for changes and executes runtests-parallel.", [], () => { - gulp.watch(["src/**/*.*", "scripts/typemock/src/**/*.*"], ["runtests-parallel"]); + gulp.watch(["src/**/*.*"], ["runtests-parallel"]); }); \ No newline at end of file diff --git a/Jakefile.js b/Jakefile.js index 8232245dcd9..60393a04e37 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -813,7 +813,7 @@ function privatePackage(packageName, prereqs) { complete(); } measure(startCompileTime); - }); + }); }, { async: true }); } @@ -821,21 +821,19 @@ function cleanPrivatePackage(packageName) { jake.rmRf(`scripts/${packageName}/dist`); } -privatePackage("typemock"); privatePackage("vfs-core"); privatePackage("vfs-errors"); privatePackage("vfs-path", ["vfs-core", "vfs-errors"]); -privatePackage("vfs", ["vfs-path", "typemock"]); +privatePackage("vfs", ["vfs-path"]); privatePackage("harness-core", ["vfs-core"]); -task("private-packages", ["typemock", "vfs", "harness-core"]); +task("private-packages", ["vfs", "harness-core"]); -task("clean-typemock", () => cleanPrivatePackage("typemock")); task("clean-vfs-core", () => cleanPrivatePackage("vfs-core")); task("clean-vfs-errors", () => cleanPrivatePackage("vfs-errors")); task("clean-vfs-path", ["clean-vfs-core", "clean-vfs-errors"], () => cleanPrivatePackage("vfs-path")); -task("clean-vfs", ["clean-vfs-path", "clean-typemock"], () => cleanPrivatePackage("vfs")); +task("clean-vfs", ["clean-vfs-path"], () => cleanPrivatePackage("vfs")); task("clean-harness-core", ["clean-vfs-core"], () => cleanPrivatePackage("harness-core")); -task("clean-private-packages", ["clean-typemock", "clean-vfs", "clean-harness-core"]); +task("clean-private-packages", ["clean-vfs", "clean-harness-core"]); // Task to build the tests infrastructure using the built compiler var run = path.join(builtLocalDirectory, "run.js"); @@ -1337,7 +1335,7 @@ desc("Runs tslint on the compiler sources. Optional arguments are: f[iles]=regex task("lint", ["build-rules"], () => { if (fold.isTravis()) console.log(fold.start("lint")); const fileMatcher = process.env.f || process.env.file || process.env.files; - + const files = fileMatcher ? `src/**/${fileMatcher}` : `Gulpfile.ts scripts/generateLocalizedDiagnosticMessages.ts "scripts/tslint/**/*.ts" "src/**/*.ts" --exclude "src/lib/*.d.ts"`; diff --git a/package.json b/package.json index b3bdec3be5d..fc836dc14a2 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "travis-fold": "latest", "ts-node": "latest", "tslint": "latest", - "typemock": "file:scripts/typemock", "typescript": "next", "vinyl": "latest", "xml2js": "^0.4.19" diff --git a/scripts/typemock/gulpfile.js b/scripts/typemock/gulpfile.js deleted file mode 100644 index 3c3833c5004..00000000000 --- a/scripts/typemock/gulpfile.js +++ /dev/null @@ -1,28 +0,0 @@ -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("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", () => gulp.watch(["src/**/*", "tsconfig.json"], ["test"])); - -gulp.task("default", ["test"]); \ No newline at end of file diff --git a/scripts/typemock/package.json b/scripts/typemock/package.json deleted file mode 100644 index e18761f0ba7..00000000000 --- a/scripts/typemock/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 deleted file mode 100644 index 368f14b3ab7..00000000000 --- a/scripts/typemock/src/arg.ts +++ /dev/null @@ -1,427 +0,0 @@ -/** - * Represents an argument condition used during verification. - */ -export class Arg { - private _validate: (arg: any) => boolean; - private _message: string | Message | List; - private _rest: boolean; - - private constructor(condition: (arg: any) => boolean, message: string | Message | List, rest = false) { - this._validate = condition; - this._message = message; - this._rest = rest; - } - - /** - * Allows any value. - */ - public static any() { - return new Arg(_ => true, `any`); - } - - /** - * Allows a value that matches the specified condition. - * @param constraint The condition used to match the value. - */ - public static is(constraint: (arg: T) => boolean) { - return new Arg(constraint, `is`); - } - - /** - * Allows only a null value. - */ - public static null() { - return new Arg(arg => arg === null, `null`); - } - - /** - * Allows only a non-null value. - */ - public static notNull() { - return Arg.not(Arg.null()); - } - - /** - * Allows only an undefined value. - */ - public static undefined() { - return new Arg(arg => arg === undefined, `undefined`); - } - - /** - * Allows only a non-undefined value. - */ - public static notUndefined() { - return Arg.not(Arg.undefined()); - } - - /** - * Allows only an undefined or null value. - */ - public static nullOrUndefined() { - return Arg.or(Arg.null(), Arg.undefined()); - } - - /** - * Allows only a non-undefined, non-null value. - */ - public static notNullOrUndefined() { - return Arg.not(Arg.nullOrUndefined()); - } - - /** - * Allows a value that matches either the specified condition, or `undefined`. - * @param condition The condition to match. - */ - public static optional(condition: T | T & Arg) { - return Arg.or(condition, Arg.undefined()); - } - - /** - * Allows any value within the provided range. - * @param min The minimum value. - * @param max The maximum value. - */ - public static between(min: T, max: T) { - return new Arg(arg => min <= arg && arg <= max, message`between ${min} and ${max}`); - } - - /** - * Allows any value in the provided array. - */ - public static in(values: object & Iterable) { - return new Arg(arg => includes(values, arg), message`in ${list(values, ", ")}`); - } - - /** - * Allows any value not in the provided array. - */ - public static notIn(values: T[]) { - return Arg.not(Arg.in(values)); - } - - /** - * Allows any value that matches the provided pattern. - */ - public static match(pattern: RegExp) { - return new Arg(arg => pattern.test(arg), message`matches ${pattern}`); - } - - /** - * Allows any string that starts with the specified substring. - */ - public static startsWith(text: string) { - return new Arg(arg => typeof arg === "string" && arg.startsWith(text), message`starts with ${text}`); - } - - /** - * Allows any string that ends with the specified substring. - */ - public static endsWith(text: string) { - return new Arg(arg => typeof arg === "string" && arg.endsWith(text), message`ends with ${text}`); - } - - /** - * Allows any string that includes the specified substring. - */ - public static includes(text: string) { - return new Arg(arg => typeof arg === "string" && arg.includes(text), message`includes ${text}`); - } - - /** - * Allows an array that matches the specified values (or `Arg` conditions), in the same order. - */ - public static array(values: (T | T & Arg)[]): T[] & Arg { - return new Arg(arg => Array.isArray(arg) && Arg.validateAll(values.map(Arg.from), arg), message`array [${list(values, ", ")}]`); - } - - /** - * 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 & null & 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) { - return new Arg(arg => typeof arg === tag, message`typeof ${tag}`); - } - - /** - * Allows any `string`. - */ - public static string() { return this.typeof("string"); } - - /** - * Allows any `number`. - */ - public static number() { return this.typeof("number"); } - - /** - * Allows any `boolean`. - */ - public static boolean() { return this.typeof("boolean"); } - - /** - * Allows any `symbol`. - */ - public static symbol() { return this.typeof("symbol"); } - - /** - * Allows any `object` (including functions but excluding `null`). - */ - public static object() { - return new Arg(arg => typeof arg === "object" && arg !== null || typeof arg === "function", `object`); - } - - /** - * Allows any `Function` value. - */ - 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) { - return new Arg(arg => arg instanceof type, message`instanceof ${type.name}`); - } - - /** - * Allows any value that has the provided property names in its prototype chain. - */ - public static has(...names: string[]) { - return new Arg(arg => count(names, name => has(arg, name)) === names.length, message`has ${list(names, ", ")}`); - } - - /** - * Allows any value that has the provided property names on itself but not its prototype chain. - */ - public static hasOwn(...names: string[]) { - return new Arg(arg => count(names, name => hasOwn(arg, name)) === names.length, message`hasOwn ${list(names, ", ")}`); - } - - /** - * Allows any array that contains the specified value. - */ - public static hasElement(value: T | T & Arg) { - return new Arg(arg => includes(arg, value), message`has element ${value}`); - } - - /** - * 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)) { - return arguments.length === 0 - ? new Arg(() => true, `rest`, /*rest*/ true) - : new Arg(arg => Arg.from(condition)._validate(arg), message`rest ${condition}`, /*rest*/ true); - } - - /** - * Negates a condition. - */ - public static not(condition: T | (T & Arg)) { - return new Arg(arg => !Arg.from(condition)._validate(arg), message`not ${condition}`); - } - - /** - * Combines conditions, where all conditions must be `true`. - */ - public static and(...conditions: ((T & Arg) | T)[]) { - return new Arg(arg => conditions.every(condition => Arg.from(condition)._validate(arg)), list(conditions, " and ")); - } - - /** - * Combines conditions, where any conditions may be `true`. - */ - public static or(...conditions: ((T & Arg) | T)[]) { - return new Arg(arg => conditions.some(condition => Arg.from(condition)._validate(arg)), list(conditions, " or ")); - } - - /** - * Ensures the value is a `Condition` - * @param value The value to coerce - * @returns The condition - */ - public static from(value: T) { - return value instanceof Arg ? value : - value === undefined ? Arg.undefined() : - value === null ? Arg.null() : - new Arg(arg => is(arg, value), new Message(() => JSON.stringify(value))); - } - - /** - * Validates an argument against a condition - * @param condition The condition to validate. - * @param arg The argument to validate against the condition. - */ - public static validate(condition: Arg, arg: any): boolean { - return condition._validate(arg); - } - - /** - * Validates the arguments against the condition. - * @param conditions The conditions to validate. - * @param args The arguments for the execution. - */ - public static validateAll(conditions: ReadonlyArray, args: ReadonlyArray): boolean { - const length = Math.max(conditions.length, args.length); - let conditionIndex = 0; - let argIndex = 0; - while (argIndex < length) { - const condition = conditionIndex < conditions.length ? conditions[conditionIndex] : undefined; - const arg = argIndex < args.length ? args[argIndex] : undefined; - if (!condition) return false; - if (argIndex >= args.length && condition._rest) return true; - if (!condition._validate(arg)) return false; - if (!condition._rest) conditionIndex++; - argIndex++; - } - return true; - } - - /** - * Gets the message associated with the provided `Arg`. - */ - public static messageFor(arg: Arg) { - return typeof arg._message === "string" ? arg._message : arg._message.toString(); - } - - /** - * 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); -} - -function message(strings: TemplateStringsArray, ...args: any[]) { - return args.some(isDeferred) - ? new Message(() => String.raw(strings, ...args.map(formatArg))) - : String.raw(strings, ...args.map(formatArg)); -} - -class Message { - private _callback: () => string; - private _message: string | undefined; - - constructor(callback: () => string) { - this._callback = callback; - } - - public toString(): string { - if (this._message === undefined) this._message = this._callback(); - return this._message; - } -} - -function list(list: Iterable, separator: string) { - return some(list, isDeferred) - ? new List(list, separator) - : formatIterableObject(list, separator); -} - -class List { - private _items: Iterable; - private _separator: string; - private _message: string | undefined; - - constructor(items: Iterable, separator: string) { - this._items = items; - this._separator = separator; - } - - public toString() { - if (this._message === undefined) this._message = formatIterableObject(this._items, this._separator); - return this._message; - } -} - -function isDeferred(arg: any): boolean { - return arg instanceof Message || arg instanceof List || arg instanceof Arg; -} - -function isIterableObject(value: any): value is Iterable { - return typeof value === "object" && value !== null && typeof value[Symbol.iterator] === "function"; -} - -function formatArg(arg: any) { - return isIterableObject(arg) ? formatIterableObject(arg, ", ") : - arg instanceof Arg ? Arg.messageFor(arg) : - arg; -} - -function formatIterableObject(arg: Iterable, separator: string) { - let result = ""; - for (const item of arg) { - if (result) result += separator; - result += item instanceof Arg ? Arg.messageFor(item) : item; - } - return result; -} - -function count(array: T[], predicate: (value: T) => boolean): number { - let result = 0; - for (const item of array) { - if (predicate(item)) result++; - } - return result; -} - -function has(object: any, key: PropertyKey) { - return Reflect.has(object, key); -} - -function hasOwn(object: any, key: PropertyKey) { - return Object.prototype.hasOwnProperty.call(object, key); -} - -function includes(object: Iterable, condition: T) { - if (condition instanceof Arg) { - for (const item of object) if (Arg.validate(condition, item)) return true; - return false; - } - - if (Array.isArray(object)) return object.indexOf(condition) >= 0; - if (object instanceof Set) return object.has(condition); - for (const item of object) if (is(item, condition)) return true; - return false; -} - -function some(object: Iterable, predicate: (a: T) => boolean) { - if (Array.isArray(object)) return object.some(predicate); - for (const item of object) if (predicate(item)) return true; - return false; -} \ No newline at end of file diff --git a/scripts/typemock/src/index.ts b/scripts/typemock/src/index.ts deleted file mode 100644 index 1626ee04e4b..00000000000 --- a/scripts/typemock/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export { Arg } from "./arg"; -export { Times } from "./times"; -export { Mock, Spy, Returns, Throws, ThisArg, Callback, Fallback, Setup, Callable, Constructable } from "./mock"; -export { Inject } from "./inject"; -export { Timers, Timer, Timeout, Interval, Immediate, AnimationFrame } from "./timers"; - -import { Mock, Spy, Callable, Constructable } from "./mock"; - -/** - * Creates a spy on an object or function. - */ -export function spy(): Mock; -/** - * Creates a spy on an object or function. - */ -export function spy(target: T): Mock; -/** - * Installs a spy on a method of an object. Use `revoke()` on the result to reset the spy. - * @param object The object containing a method. - * @param propertyKey The name of the method on the object. - */ -export function spy any }, K extends keyof T>(object: T, propertyKey: K): Spy; -export function spy any }, K extends keyof T>(object?: T, propertyKey?: K) { - return object === undefined ? Mock.spy() : propertyKey === undefined ? Mock.spy(object) : Mock.spy(object, propertyKey); -} diff --git a/scripts/typemock/src/inject.ts b/scripts/typemock/src/inject.ts deleted file mode 100644 index b1ec0ee71c6..00000000000 --- a/scripts/typemock/src/inject.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Temporarily injects a value into an object property - */ -export class Inject { - private _target: T; - private _key: K; - private _injectedValue: 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 injectedValue The value to inject - */ - constructor(target: T, propertyKey: K, injectedValue?: T[K]) { - this._target = target; - this._key = propertyKey; - this._injectedValue = arguments.length === 2 ? target[propertyKey] : injectedValue; - } - - public get target() { - return this._target; - } - - public get key() { - return this._key; - } - - public get injectedValue(): T[K] { - return this._injectedValue; - } - - public set injectedValue(value: T[K]) { - if (this._installed) { - this._target[this._key] = value; - } - this._injectedValue = 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 `injectedValue` is currently installed. - */ - public get installed(): boolean { - return this._installed; - } - - /** - * Installs `injectedValue` - */ - public install(): void { - if (this._installed) return; - this._originalValue = this._target[this._key]; - this._target[this._key] = this._injectedValue; - this._installed = true; - } - - /** - * Uninstalls `injectedValue` - */ - public uninstall(): void { - if (!this._installed) return; - this._target[this._key] = this._originalValue; - this._installed = false; - this._originalValue = null; - } - - /** - * Executes `action` with `injectedValue` installed on `target`. - */ - public static exec(target: T, propertyKey: K, injectedValue: T[K], action: () => V) { - const injector = new Inject(target, propertyKey, injectedValue); - return injector.exec(action); - } - - /** - * Executes `action` with `injectedValue` 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/mock.ts b/scripts/typemock/src/mock.ts deleted file mode 100644 index 04aeede3f99..00000000000 --- a/scripts/typemock/src/mock.ts +++ /dev/null @@ -1,580 +0,0 @@ -import { Times } from "./times"; -import { Arg } from "./arg"; -import { Inject } from "./inject"; - -const weakHandler = new WeakMap>(); -const weakMock = new WeakMap>(); - -export type Callable = (...args: any[]) => any; - -export type Constructable = new (...args: any[]) => any; - -export interface ThisArg { - this: any; -} - -export interface Returns { - return: U; -} - -export interface Fallback { - fallback: true; -} - -export interface Throws { - throw: any; -} - -export interface Callback { - callback: Callable; -} - -export type Setup = - | Returns & (ThisArg & Callback | ThisArg | Callback) - | Returns - | Throws & (ThisArg & Callback | ThisArg | Callback) - | Throws - | Fallback & (ThisArg & Callback | ThisArg | Callback) - | Fallback - | ThisArg & Callback - | ThisArg - | Callback; - -/** - * A mock version of another oject - */ -export class Mock { - private _handler: 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._handler = typeof target === "function" - ? new MockFunctionHandler() - : new MockHandler(); - - const { proxy, revoke } = Proxy.revocable(target, this._handler); - this._proxy = proxy; - this._revoke = revoke; - - weakHandler.set(proxy, this._handler); - weakMock.set(proxy, this); - - if (setups) { - this.setup(setups); - } - } - - /** - * Gets the mock version of the target - */ - public get proxy(): T { - return this._proxy; - } - - /** - * Creates an empty Mock object. - */ - public static object() { - return new Mock({}); - } - - /** - * Creates an empty Mock function. - */ - public static function() { - return new Mock(function () {}); - } - - /** - * Creates a function spy. - */ - public static spy(): Mock; - /** - * Creates a spy on an object or function. - */ - public static spy(target: T): Mock; - /** - * Installs a spy on a method of an object. Use `revoke()` on the result to reset the spy. - * @param object The object containing a method. - * @param propertyKey The name of the method on the object. - */ - public static spy any }, K extends keyof T>(object: T, propertyKey: K): Spy; - public static spy any }, K extends keyof T>(object?: T, propertyKey?: K) { - return object !== undefined && propertyKey !== undefined - ? new Spy(object, propertyKey) - : new Mock(object || function () {}); - } - - /** - * Gets the mock for an object. - * @param target The target. - */ - public static from(target: T) { - return | undefined>weakMock.get(target); - } - - /** - * 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?: Setup): this; - /** - * 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): this; - public setup(setup: Partial | ((value: T) => U), result?: Setup): this { - if (typeof setup === "function") { - this._handler.setupCall(setup, result); - } - else { - this._handler.setupMembers(setup); - } - return this; - } - - /** - * Performs verification that a specific action occurred at least once. - * @param callback A callback that simulates the expected action. - * @param message An optional message to use if verification fails. - * @returns This mock instance. - */ - public verify(callback: (value: T) => U, message?: string): 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. - * @param message An optional message to use if verification fails. - * @returns This mock instance. - */ - public verify(callback: (value: T) => U, times: Times, message?: string): 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) => U, times?: Times | string, message?: string): this { - if (typeof times === "string") { - message = times; - times = undefined; - } - if (times === undefined) { - times = Times.atLeastOnce(); - } - this._handler.verify(callback, times, message); - return this; - } - - public revoke() { - weakMock.delete(this._proxy); - weakHandler.delete(this._proxy); - this._handler.revoke(); - this._revoke(); - } -} - -export class Spy any }, K extends keyof T> extends Mock { - private _spy: Inject | undefined; - - constructor(target: T, propertyKey: K) { - super(target[propertyKey]); - this._spy = new Inject(target, propertyKey, this.proxy); - this._spy.install(); - } - - public get installed() { - return this._spy ? this._spy.installed : false; - } - - public install() { - if (!this._spy) throw new Error("Cannot install a revoked spy."); - this._spy.install(); - return this; - } - - public uninstall() { - if (this._spy) this._spy.uninstall(); - return this; - } - - public revoke() { - if (this._spy) { - this._spy.uninstall(); - this._spy = undefined; - } - super.revoke(); - } -} - -class Recording { - public static readonly noThisArg = {}; - public readonly trap: "apply" | "construct" | "invoke" | "get" | "set"; - public readonly name: PropertyKey | undefined; - public readonly thisArg: any; - public readonly argArray: ReadonlyArray; - public readonly newTarget: any; - public readonly result: Partial & Throws & Fallback> | undefined; - public readonly callback: Callable | undefined; - - private _thisCondition: Arg | undefined; - private _newTargetCondition: Arg | undefined; - private _conditions: ReadonlyArray | undefined; - - constructor(trap: "apply" | "construct" | "invoke" | "get" | "set", name: PropertyKey | undefined, thisArg: any, argArray: ReadonlyArray, newTarget: any, result: Partial & Throws & Fallback> | undefined, callback: Callable | undefined) { - this.trap = trap; - this.name = name; - this.thisArg = thisArg; - this.argArray = argArray || []; - this.newTarget = newTarget; - this.result = result; - this.callback = callback; - } - - public get thisCondition() { - return this._thisCondition || (this._thisCondition = this.thisArg === Recording.noThisArg ? Arg.any() : Arg.from(this.thisArg)); - } - - public get newTargetCondition() { - return this._newTargetCondition || (this._newTargetCondition = Arg.from(this.newTarget)); - } - - public get argConditions() { - return this._conditions || (this._conditions = this.argArray.map(Arg.from)); - } - - public get kind() { - switch (this.trap) { - case "apply": return "function"; - case "construct": return "function"; - case "invoke": return "method"; - case "get": return "property"; - case "set": return "property"; - } - } - - public static select(setups: ReadonlyArray, kind: Recording["kind"], name: PropertyKey | undefined) { - return setups.filter(setup => setup.kind === kind && setup.name === name); - } - - public static evaluate(setups: ReadonlyArray | undefined, trap: string, name: PropertyKey | undefined, thisArg: any, argArray: any[], newTarget: any, fallback: () => any) { - if (setups && setups.length > 0) { - for (const setup of setups) { - if (setup.match(trap, name, thisArg, argArray, newTarget)) { - const callback = setup.callback; - if (callback) { - Reflect.apply(callback, thisArg, argArray); - } - - const result = setup.getResult(fallback); - return trap === "set" ? true : result; - } - } - return trap === "set" ? false : undefined; - } - return fallback(); - } - - public toString(): string { - return `${this.trap} ${this.name || ""}(${this.argConditions.join(", ")})${this.newTarget ? ` [${this.newTarget.name}]` : ``}`; - } - - public match(trap: string, name: PropertyKey | undefined, thisArg: any, argArray: any, newTarget: any) { - return this.trap === trap - && this.name === name - && this.matchThisArg(thisArg) - && Arg.validateAll(this.argConditions, argArray) - && Arg.validate(this.newTargetCondition, newTarget); - } - - public matchRecording(recording: Recording) { - return this.match(recording.trap, recording.name, recording.thisArg, recording.argArray, recording.newTarget) - && this.matchResult(recording.result); - } - - private matchThisArg(thisArg: any) { - return thisArg === Recording.noThisArg - || Arg.validate(this.thisCondition, thisArg); - } - - private matchResult(result: Partial & Throws> | undefined) { - return !this.result - || this.result.return === (result && result.return) - && this.result.throw === (result && result.throw); - } - - private getResult(fallback: () => any) { - if (hasOwn(this.result, "throw")) throw this.result.throw; - if (hasOwn(this.result, "return")) return this.result.return; - if (hasOwn(this.result, "fallback")) return this.result.fallback ? fallback() : undefined; - return undefined; - } -} - -class MockHandler implements ProxyHandler { - protected readonly overrides = Object.create(null); - protected readonly recordings: Recording[] = []; - protected readonly setups: Recording[] = []; - protected readonly methodTargets = new WeakMap(); - protected readonly methodProxies = new Map(); - protected readonly methodRevocations = new Set<() => void>(); - - public get(target: T, name: PropertyKey, receiver: any = target): any { - const setups = Recording.select(this.setups, "property", name); - const result: Partial & Throws> = {}; - const recording = new Recording("get", name, target, [], /*newTarget*/ undefined, result, /*callback*/ undefined); - this.recordings.push(recording); - try { - const value = Recording.evaluate(setups, "get", name, receiver, [], /*newTarget*/ undefined, - () => Reflect.get(this.getTarget(target, name), name, receiver)); - return typeof value === "function" ? this.getMethod(name, value) : value; - } - catch (e) { - throw result.throw = e; - } - } - - public set(target: T, name: PropertyKey, value: any, receiver: any = target): boolean { - if (typeof value === "function" && this.methodTargets.has(value)) { - value = this.methodTargets.get(value); - } - - const setups = Recording.select(this.setups, "property", name); - const result: Partial & Throws> = {}; - const recording = new Recording("set", name, target, [value], /*newTarget*/ undefined, result, /*callback*/ undefined); - this.recordings.push(recording); - try { - const success = Recording.evaluate(setups, "set", name, receiver, [value], /*newTarget*/ undefined, - () => Reflect.set(this.getTarget(target, name), name, value, receiver)); - result.return = undefined; - return success; - } - catch (e) { - throw result.throw = e; - } - } - - public invoke(proxy: T, name: PropertyKey, method: Function, argArray: any[]): any { - const setups = Recording.select(this.setups, "method", name); - const result: Partial & Throws> = {}; - const recording = new Recording("invoke", name, proxy, argArray, /*newTarget*/ undefined, result, /*callback*/ undefined); - this.recordings.push(recording); - try { - return Recording.evaluate(setups, "invoke", name, proxy, argArray, /*newTarget*/ undefined, - () => Reflect.apply(method, proxy, argArray)); - } - catch (e) { - throw result.throw = e; - } - } - - public setupCall(callback: (value: any) => any, result: Setup | undefined) { - const recording = this.capture(callback, result); - const existing = this.setups.find(setup => setup.name === recording.name); - if (existing) { - if (existing.kind !== recording.kind) { - throw new Error(`Cannot mix method and property setups for the same member name.`); - } - } - else if (recording.name !== undefined) { - if (recording.kind === "method") { - this.defineMethod(recording.name); - } - else if (recording.kind === "property") { - this.defineAccessor(recording.name); - } - } - - this.setups.push(recording); - } - - 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) => U, times: Times, message?: string): void { - const expectation = this.capture(callback, /*result*/ undefined); - - let count: number = 0; - for (const recording of this.recordings) { - if (expectation.matchRecording(recording)) { - count++; - } - } - - times.check(count, message || `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(); - } - } - - protected capture(callback: (value: T) => U, result: Setup | undefined) { - return this.captureCore({}, new CapturingHandler(result), callback); - } - - protected captureCore(target: T, handler: CapturingHandler, callback: (value: T) => U): Recording { - const { proxy, revoke } = Proxy.revocable(target, handler); - try { - callback(proxy); - if (!handler.recording) { - throw new Error("Nothing was captured."); - } - return handler.recording; - } - finally { - revoke(); - } - } - - private defineMethod(name: PropertyKey) { - const setups = this.setups; - this.setupMembers({ - [name](...argArray: any[]) { - return Recording.evaluate(setups, "invoke", name, this, argArray, /*newTarget*/ undefined, () => {}); - } - }); - } - - private defineAccessor(name: PropertyKey) { - const setups = this.setups; - this.setupMembers({ - get [name]() { - return Recording.evaluate(setups, "get", name, this, [], /*newTarget*/ undefined, () => {}); - }, - set [name](value: any) { - Recording.evaluate(setups, "set", name, this, [value], /*newTarget*/ undefined, () => {}); - } - }); - } -} - -class MockFunctionHandler extends MockHandler { - public apply(target: T, thisArg: any, argArray: any[]): any { - const setups = Recording.select(this.setups, "function", /*name*/ undefined); - const result: Partial & Throws> = {}; - const recording = new Recording("apply", /*name*/ undefined, thisArg, argArray, /*newTarget*/ undefined, result, /*callback*/ undefined); - this.recordings.push(recording); - try { - return Recording.evaluate(setups, "apply", /*name*/ undefined, thisArg, argArray, /*newTarget*/ undefined, - () => Reflect.apply(target, thisArg, argArray)); - } - catch (e) { - throw result.throw = e; - } - } - - public construct(target: T, argArray: any[], newTarget?: any): any { - const setups = Recording.select(this.setups, "function", /*name*/ undefined); - const result: Partial & Throws> = {}; - const recording = new Recording("construct", /*name*/ undefined, /*thisArg*/ undefined, argArray, newTarget, result, /*callback*/ undefined); - this.recordings.push(recording); - try { - return Recording.evaluate(setups, "construct", /*name*/ undefined, /*thisArg*/ undefined, argArray, newTarget, - () => Reflect.construct(target, argArray, newTarget)); - } - catch (e) { - throw result.throw = e; - } - } - - protected capture(callback: (value: T) => U, result: Returns & ThisArg | Returns | Throws & ThisArg | Throws | ThisArg | undefined) { - return this.captureCore(function() {}, new CapturingFunctionHandler(result), callback); - } -} - -class MethodHandler { - public name: PropertyKey; - - constructor(name: PropertyKey) { - this.name = name; - } - - public apply(target: Function, thisArgument: any, argumentsList: any[]): any { - const handler = weakHandler.get(thisArgument); - return handler - ? handler.invoke(thisArgument, this.name, target, argumentsList) - : Reflect.apply(target, thisArgument, argumentsList); - } -} - -class CapturingHandler implements ProxyHandler { - public recording: Recording | undefined; - - protected readonly callback: Callable | undefined; - protected readonly thisArg: any; - protected readonly result: Returns | Throws | Fallback | undefined; - - constructor(result: Partial & Throws & ThisArg & Callback & Fallback> | undefined) { - this.thisArg = hasOwn(result, "this") ? result.this : Recording.noThisArg; - this.callback = hasOwn(result, "callback") ? result.callback : undefined; - this.result = hasOwn(result, "return") ? { return: result.return } : - hasOwn(result, "throw") ? { throw: result.throw } : - hasOwn(result, "fallback") && result.fallback ? { fallback: true } : - undefined; - } - - public get(_target: T, name: PropertyKey, _receiver: any): any { - this.recording = new Recording("get", name, this.thisArg, [], /*newTarget*/ undefined, this.result, this.callback); - return (...argArray: any[]) => { this.recording = new Recording("invoke", name, this.thisArg, argArray, /*newTarget*/ undefined, this.result, this.callback); }; - } - - public set(_target: T, name: PropertyKey, value: any, _receiver: any): boolean { - this.recording = new Recording("set", name, this.thisArg, [value], /*newTarget*/ undefined, this.result, this.callback); - return true; - } -} - -class CapturingFunctionHandler extends CapturingHandler { - public apply(_target: T, _thisArg: any, argArray: any[]): any { - this.recording = new Recording("apply", /*name*/ undefined, this.thisArg, argArray, /*newTarget*/ undefined, this.result, this.callback); - return undefined; - } - - public construct(_target: T, argArray: any[], newTarget?: any): any { - this.recording = new Recording("construct", /*name*/ undefined, /*thisArg*/ undefined, argArray, newTarget, this.result, this.callback); - return undefined; - } -} - -function hasOwn(object: Partial | undefined, key: K): object is (T | T & never) & { [P in K]: T[P] } { - return object !== undefined - && Object.prototype.hasOwnProperty.call(object, key); -} \ No newline at end of file diff --git a/scripts/typemock/src/tests/argTests.ts b/scripts/typemock/src/tests/argTests.ts deleted file mode 100644 index 03d96f1c469..00000000000 --- a/scripts/typemock/src/tests/argTests.ts +++ /dev/null @@ -1,633 +0,0 @@ -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"); - - // assert - assert.isTrue(result); - }); - 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"); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.is(value => value === "a")); - - // act - const result = Arg.validate(target, "b"); - - // assert - assert.isFalse(result); - }); - 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, {}); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.notNull()); - - // act - const result = Arg.validate(target, null); - - // assert - assert.isFalse(result); - }); - 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); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.null()); - - // act - const result = Arg.validate(target, {}); - - // assert - assert.isFalse(result); - }); - 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, {}); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.notUndefined()); - - // act - const result = Arg.validate(target, undefined); - - // assert - assert.isFalse(result); - }); - 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); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.undefined()); - - // act - const result = Arg.validate(target, {}); - - // assert - assert.isFalse(result); - }); - 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, {}); - - // assert - assert.isTrue(result); - }); - it("invalid (null)", () => { - // arrange - const target = Arg.from(Arg.notNullOrUndefined()); - - // act - const result = Arg.validate(target, null); - - // assert - assert.isFalse(result); - }); - it("invalid (undefined)", () => { - // arrange - const target = Arg.from(Arg.notNullOrUndefined()); - - // act - const result = Arg.validate(target, undefined); - - // assert - assert.isFalse(result); - }); - 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); - - // assert - assert.isTrue(result); - }); - it("valid (undefined)", () => { - // arrange - const target = Arg.from(Arg.nullOrUndefined()); - - // act - const result = Arg.validate(target, undefined); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.nullOrUndefined()); - - // act - const result = Arg.validate(target, {}); - - // assert - assert.isFalse(result); - }); - 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); - const mid = Arg.validate(target, 2); - const max = Arg.validate(target, 3); - - // assert - assert.isTrue(min); - assert.isTrue(mid); - assert.isTrue(max); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.between(1, 3)); - - // act - const before = Arg.validate(target, 0); - const after = Arg.validate(target, 4); - - // assert - assert.isFalse(before); - assert.isFalse(after); - }); - 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"); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.in(["a", "b"])); - - // act - const result = Arg.validate(target, "c"); - - // assert - assert.isFalse(result); - }); - 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"); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.notIn(["a", "b"])); - - // act - const result = Arg.validate(target, "a"); - - // assert - assert.isFalse(result); - }); - 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"); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.match(/^a$/)); - - // act - const result = Arg.validate(target, "b"); - - // assert - assert.isFalse(result); - }); - 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); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.typeof("number")); - - // act - const result = Arg.validate(target, "a"); - - // assert - assert.isFalse(result); - }); - 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()); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - class C {} - const target = Arg.from(Arg.instanceof(C)); - - // act - const result = Arg.validate(target, {}); - - // assert - assert.isFalse(result); - }); - 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 }); - const proto = Arg.validate(target, { __proto__: { a: 1 } }); - - // assert - assert.isTrue(own); - assert.isTrue(proto); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.has("a")); - - // act - const result = Arg.validate(target, { b: 1 }); - - // assert - assert.isFalse(result); - }); - 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 }); - - // assert - assert.isTrue(own); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.hasOwn("a")); - - // act - const result = Arg.validate(target, { b: 1 }); - const proto = Arg.validate(target, { __proto__: { a: 1 } }); - - // assert - assert.isFalse(result); - assert.isFalse(proto); - }); - 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.validateAll([target], []); - const multiple = Arg.validateAll([target], ["a", "b"]); - - // assert - assert.isTrue(empty); - assert.isTrue(multiple); - }); - 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.validateAll([target], []); - const multiple = Arg.validateAll([target], ["a", "b"]); - - // assert - assert.isTrue(empty); - assert.isTrue(multiple); - }); - it("invalid", () => { - // arrange - const target = Arg.from(Arg.rest(Arg.typeof("string"))); - - // act - const result = Arg.validateAll([target], ["a", 1]); - - // assert - assert.isFalse(result); - }); - 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"); - - // assert - assert.isTrue(result); - }); - it("invalid", () => { - // arrange - const target = Arg.from("a"); - - // act - const result = Arg.validate(target, "b"); - - // assert - assert.isFalse(result); - }); - 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 deleted file mode 100644 index 1ff30bfad40..00000000000 --- a/scripts/typemock/src/tests/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "./argTests"; -import "./timesTests"; -import "./mockTests"; -import "./injectTests"; -import "./timersTests"; \ No newline at end of file diff --git a/scripts/typemock/src/tests/injectTests.ts b/scripts/typemock/src/tests/injectTests.ts deleted file mode 100644 index c7aa747e314..00000000000 --- a/scripts/typemock/src/tests/injectTests.ts +++ /dev/null @@ -1,79 +0,0 @@ -import "./sourceMapSupport"; -import { Mock } from "../mock"; -import { Inject } from "../inject"; -import { Times } from "../times"; -import { assert } from "chai"; - -describe("inject", () => { - it("install replaces value", () => { - // arrange - const mock = new Mock({ a: 1 }); - const inject = new Inject(mock.proxy, "a", 2); - - // act - inject.install(); - - // assert - mock.verify(_ => _.a = 2, Times.once()); - }); - it("install is installed", () => { - // arrange - const mock = new Mock({ a: 1 }); - const inject = new Inject(mock.proxy, "a", 2); - - // act - inject.install(); - - // assert - assert.isTrue(inject.installed); - }); - it("install twice only installs once", () => { - // arrange - const mock = new Mock({ a: 1 }); - const inject = new Inject(mock.proxy, "a", 2); - - // act - inject.install(); - inject.install(); - - // assert - mock.verify(_ => _.a = 2, Times.once()); - }); - it("uninstall restores value", () => { - // arrange - const mock = new Mock({ a: 1 }); - const inject = new Inject(mock.proxy, "a", 2); - inject.install(); - - // act - inject.uninstall(); - - // assert - mock.verify(_ => _.a = 1, Times.once()); - }); - it("uninstall is not installed", () => { - // arrange - const mock = new Mock({ a: 1 }); - const inject = new Inject(mock.proxy, "a", 2); - inject.install(); - - // act - inject.uninstall(); - - // assert - assert.isFalse(inject.installed); - }); - it("uninstall twice only uninstalls once", () => { - // arrange - const mock = new Mock({ a: 1 }); - const inject = new Inject(mock.proxy, "a", 2); - inject.install(); - - // act - inject.uninstall(); - inject.uninstall(); - - // assert - mock.verify(_ => _.a = 1, Times.once()); - }); -}); diff --git a/scripts/typemock/src/tests/mockTests.ts b/scripts/typemock/src/tests/mockTests.ts deleted file mode 100644 index 26fcfd245f2..00000000000 --- a/scripts/typemock/src/tests/mockTests.ts +++ /dev/null @@ -1,362 +0,0 @@ -import "./sourceMapSupport"; -import { Mock } from "../mock"; -import { Arg } from "../arg"; -import { Times } from "../times"; -import { recordError } from "./utils"; -import { assert } from "chai"; - -describe("mock", () => { - describe("no setup", () => { - it("get (exists)", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target); - - // act - const result = mock.proxy.a; - - // assert - assert.equal(result, 1); - }); - it("get (missing)", () => { - // arrange - const mock = new Mock<{ a?: number }>(); - - // act - const result = mock.proxy.a; - - // assert - assert.isUndefined(result); - }); - it("set (exists)", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target); - - // act - mock.proxy.a = 2; - const result = mock.proxy.a; - - // assert - assert.equal(result, 2); - }); - it("set (missing)", () => { - // arrange - const mock = new Mock<{ a?: number }>(); - - // act - mock.proxy.a = 2; - const result = mock.proxy.a; - - // assert - assert.equal(result, 2); - }); - it("method (existing)", () => { - // arrange - const target = { a() { return 1; } }; - const mock = new Mock(target); - - // act - const result = mock.proxy.a(); - - // assert - assert.equal(1, result); - }); - }); - - describe("setup", () => { - describe("using object", () => { - it("get", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target, { get a() { return 2; } }); - - // act - const result = mock.proxy.a; - - // assert - assert.equal(2, result); - }); - it("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.proxy.a); - - // assert - assert.strictEqual(error, e); - }); - it("set", () => { - // arrange - let _a: number | undefined; - const target = { a: 1 }; - const mock = new Mock(target, { set a(value: number) { _a = value; } }); - - // act - mock.proxy.a = 2; - - // assert - assert.equal(2, _a); - assert.equal(1, target.a); - }); - it("set with throw", () => { - // arrange - const target = { a: 1 }; - const error = new Error("error"); - const mock = new Mock(target, { set a(_: number) { throw error; } }); - - // act - const e = recordError(() => mock.proxy.a = 2); - - // assert - assert.strictEqual(error, e); - }); - it("method", () => { - // arrange - const target = { a() { return 1; } }; - const mock = new Mock(target, { a() { return 2; } }); - - // act - const result = mock.proxy.a(); - - // assert - assert.equal(2, result); - }); - it("method 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.proxy.a()); - - // assert - assert.strictEqual(error, e); - }); - it("new property", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target, { b: 2 }); - - // act - const result = (mock.proxy).b; - - // assert - assert.equal(2, result); - }); - it("new method", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target, { b() { return 2; } }); - - // act - const result = (mock.proxy).b(); - - // assert - assert.equal(2, result); - }); - }); - describe("using callback", () => { - it("get only", () => { - // arrange - const mock = new Mock<{ a: number }>(); - mock.setup(_ => _.a, { return: 2 }); - - // act - const result1 = mock.proxy.a; - const err = recordError(() => mock.proxy.a = 3); - const result2 = mock.proxy.a; - - // assert - assert.strictEqual(result1, 2); - assert.strictEqual(result2, 2); - assert.instanceOf(err, Error); - }); - it("set only", () => { - // arrange - const mock = new Mock<{ a: number }>(); - mock.setup(_ => _.a = 2); - - // act - const result = mock.proxy.a; - const err2 = recordError(() => mock.proxy.a = 2); - const err3 = recordError(() => mock.proxy.a = 3); - - // assert - assert.isUndefined(result); - assert.isUndefined(err2); - assert.instanceOf(err3, Error); - }); - it("get and set", () => { - // arrange - const mock = new Mock<{ a: number }>(); - mock.setup(_ => _.a, { return: 2 }); - mock.setup(_ => _.a = Arg.any()); - - // act - const result = mock.proxy.a; - mock.proxy.a = 3; - - // assert - assert.strictEqual(result, 2); - }); - it("method", () => { - // arrange - const mock = new Mock<{ a(x: number): number; }>(); - mock.setup(_ => _.a(1), { return: 2 }); - - // act - const result = mock.proxy.a(1); - - // assert - assert.strictEqual(result, 2); - }); - it("function", () => { - // arrange - const mock = new Mock<(x: number) => number>(_ => 0); - mock.setup(_ => _(Arg.number()), { return: 2 }); - - // act - const result = mock.proxy(1); - - // assert - assert.strictEqual(result, 2); - }); - }); - }); - - describe("verify", () => { - describe("no setup", () => { - describe("get", () => { - it("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("called passes", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target); - mock.proxy.a; - - // act - const e = recordError(() => mock.verify(_ => _.a, Times.once())); - - // assert - assert.isUndefined(e); - }); - }); - describe("set", () => { - it("not called throws", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target); - - // act - const e = recordError(() => mock.verify(_ => _.a = 2, Times.once())); - - // assert - assert.instanceOf(e, Error); - }); - it("called passes", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target); - mock.proxy.a = 2; - - // act - const e = recordError(() => mock.verify(_ => _.a = 2, Times.once())); - - // assert - assert.isUndefined(e); - }); - }); - describe("method", () => { - it("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("called passes", () => { - // arrange - const target = { a() { return 1; } }; - const mock = new Mock(target); - mock.proxy.a(); - - // act - const e = recordError(() => mock.verify(_ => _.a(), Times.once())); - - // assert - assert.isUndefined(e); - }); - }); - describe("function", () => { - it("not called throws", () => { - // arrange - const mock = Mock.function(); - - // act - const e = recordError(() => mock.verify(_ => _(), Times.once())); - - // assert - assert.instanceOf(e, Error); - }); - it("called passes", () => { - // arrange - const mock = Mock.function(); - mock.proxy(); - - // act - const e = recordError(() => mock.verify(_ => _(), Times.once())); - - // assert - assert.isUndefined(e); - }); - }); - }); - it("setup get, called passes", () => { - // arrange - const target = { a: 1 }; - const mock = new Mock(target, { get a() { return 2 } }); - mock.proxy.a; - - // act - const e = recordError(() => mock.verify(_ => _.a, Times.once())); - - // assert - assert.isUndefined(e); - }); - it("setup method, called passes", () => { - // arrange - const target = { a(x: number) { return x + 1; } }; - const mock = new Mock(target, { - a(x: number) { - return x + 2; - } - }); - mock.proxy.a(3); - - // act - const e = recordError(() => mock.verify(_ => _.a(Arg.number()), Times.once())); - - // assert - assert.isUndefined(e); - }); - }); -}); \ No newline at end of file diff --git a/scripts/typemock/src/tests/sourceMapSupport.ts b/scripts/typemock/src/tests/sourceMapSupport.ts deleted file mode 100644 index 475edf419f7..00000000000 --- a/scripts/typemock/src/tests/sourceMapSupport.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { install } from "source-map-support"; - -install(); \ No newline at end of file diff --git a/scripts/typemock/src/tests/timersTests.ts b/scripts/typemock/src/tests/timersTests.ts deleted file mode 100644 index 15c8161facc..00000000000 --- a/scripts/typemock/src/tests/timersTests.ts +++ /dev/null @@ -1,305 +0,0 @@ -import "./sourceMapSupport"; -import { Mock } from "../mock"; -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 = Mock.function(); - - // act - const handle = target.setImmediate(spy.proxy); - const pending = target.getPending(); - - // assert - assert.strictEqual(pending.length, 1); - assert.strictEqual(pending[0].kind, "immediate"); - assert.isDefined(handle); - spy.verify(_ => _(), Times.none()); - }); - it("set/clear", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - const handle = target.setImmediate(spy.proxy); - target.clearImmedate(handle); - const pending = target.getPending(); - - // assert - assert.strictEqual(pending.length, 0); - spy.verify(_ => _(), Times.none()); - }); - it("set one and execute", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.setImmediate(spy.proxy); - const count = target.executeImmediates(); - - // assert - assert.strictEqual(count, 1); - spy.verify(_ => _(), Times.once()); - }); - it("set one with arg and execute", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.setImmediate(spy.proxy, "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 = Mock.spy(() => { target.setImmediate(spy.proxy); }); - - // act - target.setImmediate(spy.proxy); - const count = target.executeImmediates(/*maxDepth*/ 0); - - // assert - assert.strictEqual(count, 1); - spy.verify(_ => _(), Times.once()); - }); - it("nested with maxDepth = 1", () => { - // arrange - const target = new Timers(); - const spy = Mock.spy(() => { target.setImmediate(spy.proxy); }); - - // act - target.setImmediate(spy.proxy); - const count = target.executeImmediates(/*maxDepth*/ 1); - - // assert - assert.strictEqual(count, 2); - spy.verify(_ => _(), Times.exactly(2)); - }); - }); - describe("timeout", () => { - it("set adds entry, does not invoke", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - const handle = target.setTimeout(spy.proxy, 0); - const pending = target.getPending(); - - // assert - assert.strictEqual(pending.length, 1); - assert.strictEqual(pending[0].kind, "timeout"); - assert.isDefined(handle); - spy.verify(_ => _(), Times.none()); - }); - it("set/clear", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - const handle = target.setTimeout(spy.proxy, 0); - target.clearTimeout(handle); - const pending = target.getPending(); - - // assert - assert.strictEqual(pending.length, 0); - spy.verify(_ => _(), Times.none()); - }); - it("set adds future entry, advance prior to due does not invoke", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.setTimeout(spy.proxy, 10); - const count = target.advance(9); - - // assert - assert.strictEqual(count, 0); - spy.verify(_ => _(), Times.none()); - }); - it("set adds future entry, advance to due invokes", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.setTimeout(spy.proxy, 10); - const count = target.advance(10); - - // assert - assert.strictEqual(count, 1); - spy.verify(_ => _(), Times.once()); - }); - it("5 nested sets throttle", () => { - // arrange - const target = new Timers(); - const spy = new Mock(() => { target.setTimeout(spy.proxy, 0); }); - - // act - target.setTimeout(spy.proxy, 0); - const count = target.advance(1); - - // assert - assert.strictEqual(count, 5); - spy.verify(_ => _(), Times.exactly(5)); - }); - }); - describe("interval", () => { - it("set adds entry, does not invoke", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - const handle = target.setInterval(spy.proxy, 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.verify(_ => _(), Times.none()); - }); - it("set/clear", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - const handle = target.setInterval(spy.proxy, 0); - target.clearInterval(handle); - const pending = target.getPending({ kind: "interval", ms: 10 }); - - // assert - assert.strictEqual(pending.length, 0); - spy.verify(_ => _(), Times.none()); - }); - it("set adds future entry, advance prior to due does not invoke", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.setInterval(spy.proxy, 10); - const count = target.advance(9); - - // assert - assert.strictEqual(count, 0); - spy.verify(_ => _(), Times.none()); - }); - it("set adds future entry, advance to due invokes", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.setInterval(spy.proxy, 10); - const count = target.advance(10); - - // assert - assert.strictEqual(count, 1); - spy.verify(_ => _(), Times.once()); - }); - it("set adds future entry, advance to due twice invokes twice", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.setInterval(spy.proxy, 10); - const count = target.advance(20); - - // assert - assert.strictEqual(count, 2); - spy.verify(_ => _(), Times.exactly(2)); - }); - it("set adds future entry, remove before second due time", () => { - // arrange - const target = new Timers(); - const spy = new Mock(() => { target.clearInterval(handle); }); - - // act - const handle = target.setInterval(spy.proxy, 10); - const count = target.advance(20); - - // assert - assert.strictEqual(count, 1); - spy.verify(_ => _(), Times.exactly(1)); - }); - }); - describe("frame", () => { - it("request adds entry, does not invoke", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - const handle = target.requestAnimationFrame(spy.proxy); - const pending = target.getPending({ ms: 16 }); - - // assert - assert.strictEqual(pending.length, 1); - assert.strictEqual(pending[0].kind, "frame"); - assert.isDefined(handle); - spy.verify(_ => _(), Times.none()); - }); - it("request/cancel", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - const handle = target.requestAnimationFrame(spy.proxy); - target.cancelAnimationFrame(handle); - const pending = target.getPending(); - - // assert - assert.strictEqual(pending.length, 0); - spy.verify(_ => _(), Times.none()); - }); - it("request and advance past one frame", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.requestAnimationFrame(spy.proxy); - const count = target.advance(16); - - // assert - assert.strictEqual(count, 1); - spy.verify(_ => _(Arg.number()), Times.once()); - }); - it("requests clamped to 16ms", () => { - // arrange - const target = new Timers(); - const spy = Mock.function(); - - // act - target.requestAnimationFrame(spy.proxy); - target.advance(10); - target.requestAnimationFrame(spy.proxy); - const count = target.advance(16); - - // assert - assert.strictEqual(count, 2); - spy.verify(_ => _(Arg.number()), 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 deleted file mode 100644 index 4cce2500cb7..00000000000 --- a/scripts/typemock/src/tests/timesTests.ts +++ /dev/null @@ -1,236 +0,0 @@ -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 deleted file mode 100644 index d28423a2887..00000000000 --- a/scripts/typemock/src/tests/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 2dd22781387..00000000000 --- a/scripts/typemock/src/timers.ts +++ /dev/null @@ -1,479 +0,0 @@ -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) { - if (maxDepth >= 0) { - count += this.executeImmediates(maxDepth); - 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.advance(this.remainingTime, maxDepth); - } - - /** - * 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 deleted file mode 100644 index 318d4702dc4..00000000000 --- a/scripts/typemock/src/times.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * 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/tsconfig.json b/scripts/typemock/tsconfig.json deleted file mode 100644 index 0cac8602976..00000000000 --- a/scripts/typemock/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es2015", - "strict": true, - "declaration": true, - "sourceMap": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "types": ["mocha"], - "newLine": "LF", - "outDir": "dist" - }, - "include": [ - "src/**/*" - ] -} \ No newline at end of file diff --git a/scripts/vfs-core/src/collections.ts b/scripts/vfs-core/src/collections.ts index 5c4fa94d5d5..f73ae6c4fd4 100644 --- a/scripts/vfs-core/src/collections.ts +++ b/scripts/vfs-core/src/collections.ts @@ -27,6 +27,10 @@ export class SortedMap { return this._keys.length; } + public get comparer() { + return this._comparer; + } + public get [Symbol.toStringTag]() { return "SortedMap"; } @@ -226,6 +230,10 @@ export class SortedSet { return this._values.length; } + public get comparer() { + return this._comparer; + } + public get [Symbol.toStringTag]() { return "SortedSet"; } diff --git a/scripts/vfs/package.json b/scripts/vfs/package.json index 3f3ff706bb0..0ba2ebea236 100644 --- a/scripts/vfs/package.json +++ b/scripts/vfs/package.json @@ -17,7 +17,6 @@ "@types/mocha": "^2.2.44", "@types/node": "^8.0.20", "@types/source-map-support": "^0.4.0", - "typemock": "file:../typemock", "chai": "^4.1.2", "del": "^2.0.2", "gulp": "^3.9.1", diff --git a/scripts/vfs/src/tests/fileSystemTests.ts b/scripts/vfs/src/tests/fileSystemTests.ts index e8ba11abcde..2d570edc996 100644 --- a/scripts/vfs/src/tests/fileSystemTests.ts +++ b/scripts/vfs/src/tests/fileSystemTests.ts @@ -1,6 +1,6 @@ import * as vpath from "@typescript/vfs-path"; import * as constants from "../constants"; -import { spy, Times, Arg } from "typemock"; +import { createSpy } from "./utils"; import { FileSystem } from "../fileSystem"; import { Link, Symlink } from "../fileSet"; import { assert } from "chai"; @@ -832,528 +832,579 @@ describe("fileSystem", () => { describe("directory (non-recursive)", () => { it("open() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {} } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.openSync("dir/file", "w+"); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("open() + write() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {} } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); const fd = fs.openSync("dir/file", "w+"); fs.writeSync(fd, Buffer.from("test"), 0, 4); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("open() + write() + close() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {} } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); const fd = fs.openSync("dir/file", "w+"); fs.writeSync(fd, Buffer.from("test"), 0, 4); fs.closeSync(fd); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["change", "file"] + ]); }); it("open() + close() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {} } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); const fd = fs.openSync("dir/file", "w+"); fs.closeSync(fd); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["change", "file"] + ]); }); it("writeFile() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {} } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.writeFileSync("dir/file", "test"); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["change", "file"] + ]); }); it("writeFile() replace file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.writeFileSync("dir/file", "test"); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "file"] + ]); }); it("truncate() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.truncateSync(vpath.combine(root, "dir/file")); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "file"] + ]); }); it("rename() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.renameSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _("rename", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["rename", "file1"] + ]); }); it("link() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.linkSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file1"] + ]); }); it("symlink() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.symlinkSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file1"] + ]); }); it("unlink() file (single link)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/file")); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("unlink() file (multiple links)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "", "file1": new Link("file") } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/file")); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("mkdir() new subdirectory", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.mkdirSync(vpath.combine(root, "dir/subdir")); - callbackSpy.verify(_ => _("rename", "subdir"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "subdir"] + ]); }); it("rmdir() subdirectory", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "subdir": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), callbackSpy.proxy); fs.rmdirSync(vpath.combine(root, "dir/subdir")); - callbackSpy.verify(_ => _("rename", "subdir"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "subdir"] + ]); }); }); describe("directory (recursive)", () => { it("open() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.openSync("dir/sub1/file1", "w+"); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1"] + ]); }); it("open() + write() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); const fd = fs.openSync("dir/sub1/file1", "w+"); fs.writeSync(fd, Buffer.from("test"), 0, 4); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1"] + ]); }); it("open() + write() + close() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); const fd = fs.openSync("dir/sub1/file1", "w+"); fs.writeSync(fd, Buffer.from("test"), 0, 4); fs.closeSync(fd); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(3)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1/file1"], + ["change", "sub1"] + ]); }); it("open() + close() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); const fd = fs.openSync("dir/sub1/file1", "w+"); fs.closeSync(fd); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1"], + ["change", "sub1/file1"] + ]); }); it("writeFile() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.writeFileSync("dir/sub1/file1", "test"); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(3)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1/file1"], + ["change", "sub1"] + ]); }); it("writeFile() replace file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "" } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.writeFileSync("dir/sub1/file1", "test"); - callbackSpy.verify(_ => _("change", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "sub1/file1"] + ]); }); it("truncate() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "test" } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.truncateSync(vpath.combine(root, "dir/sub1/file1")); - callbackSpy.verify(_ => _("change", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "sub1/file1"] + ]); }); it("rename() file (same directory)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "test" } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.renameSync(vpath.combine(root, "dir/sub1/file1"), vpath.combine(root, "dir/sub1/file2")); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("rename", "sub1/file2"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(3)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["rename", "sub1/file2"], + ["change", "sub1"] + ]); }); it("rename() file (different directory)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "test" }, "sub2": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.renameSync(vpath.combine(root, "dir/sub1/file1"), vpath.combine(root, "dir/sub2/file1")); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("rename", "sub2/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub2"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(4)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["rename", "sub2/file1"], + ["change", "sub1"], + ["change", "sub2"] + ]); }); it("link() file (same directory)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "test" } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.linkSync(vpath.combine(root, "dir/sub1/file1"), vpath.combine(root, "dir/sub1/file2")); - callbackSpy.verify(_ => _("rename", "sub1/file2"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file2"], + ["change", "sub1"] + ]); }); it("link() file (different directory)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "test" }, "sub2": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.linkSync(vpath.combine(root, "dir/sub1/file1"), vpath.combine(root, "dir/sub2/file1")); - callbackSpy.verify(_ => _("rename", "sub2/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub2"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub2/file1"], + ["change", "sub2"] + ]); }); it("symlink() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file": "test" } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.symlinkSync(vpath.combine(root, "dir/sub1/file"), vpath.combine(root, "dir/sub1/file1")); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1"] + ]); }); it("unlink() file (single link)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "" } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/sub1/file1")); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1"] + ]); }); it("unlink() file (multiple links)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "file1": "", "file2": new Link("file1") } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/sub1/file1")); - callbackSpy.verify(_ => _("rename", "sub1/file1"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/file1"], + ["change", "sub1"] + ]); }); it("mkdir() new subdirectory", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": {} } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.mkdirSync(vpath.combine(root, "dir/sub1/sub2")); - callbackSpy.verify(_ => _("rename", "sub1/sub2"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/sub2"], + ["change", "sub1"] + ]); }); it("rmdir() subdirectory", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "sub1": { "sub2": {} } } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir"), { recursive: true }, callbackSpy.proxy); fs.rmdirSync(vpath.combine(root, "dir/sub1/sub2")); - callbackSpy.verify(_ => _("rename", "sub1/sub2"), Times.once()); - callbackSpy.verify(_ => _("change", "sub1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "sub1/sub2"], + ["change", "sub1"] + ]); }); }); describe("file", () => { it("writeFile() replace", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file1": "" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir/file1"), callbackSpy.proxy); fs.writeFileSync("dir/file1", "test"); - callbackSpy.verify(_ => _("change", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "file1"] + ]); }); it("unlink() (single link)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file1": "" } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir/file1"), callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file1"] + ]); }); }); describe("symlink (directory, non-recursive)", () => { it("open() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {}, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.openSync("dir/file", "w+"); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("open() + write() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {}, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); const fd = fs.openSync("dir/file", "w+"); fs.writeSync(fd, Buffer.from("test"), 0, 4); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("open() + write() + close() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {}, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); const fd = fs.openSync("dir/file", "w+"); fs.writeSync(fd, Buffer.from("test"), 0, 4); fs.closeSync(fd); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["change", "file"] + ]); }); it("open() + close() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {}, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); const fd = fs.openSync("dir/file", "w+"); fs.closeSync(fd); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["change", "file"] + ]); }); it("writeFile() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": {}, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.writeFileSync("dir/file", "test"); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["change", "file"] + ]); }); it("writeFile() replace file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.writeFileSync("dir/file", "test"); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "file"] + ]); }); it("truncate() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.truncateSync(vpath.combine(root, "dir/file")); - callbackSpy.verify(_ => _("change", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "file"] + ]); }); it("rename() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.renameSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _("rename", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.exactly(2)); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"], + ["rename", "file1"] + ]); }); it("link() new file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.linkSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file1"] + ]); }); it("symlink() file", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "test" }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.symlinkSync(vpath.combine(root, "dir/file"), vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file1"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file1"] + ]); }); it("unlink() file (single link)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "" }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/file")); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("unlink() file (multiple links)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file": "", "file1": new Link("file") }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/file")); - callbackSpy.verify(_ => _("rename", "file"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file"] + ]); }); it("mkdir() new subdirectory", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.mkdirSync(vpath.combine(root, "dir/subdir")); - callbackSpy.verify(_ => _("rename", "subdir"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "subdir"] + ]); }); it("rmdir() subdirectory", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "subdir": {} }, "dir2": new Symlink("dir") } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir2"), callbackSpy.proxy); fs.rmdirSync(vpath.combine(root, "dir/subdir")); - callbackSpy.verify(_ => _("rename", "subdir"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "subdir"] + ]); }); }); describe("symlink (file)", () => { it("writeFile() replace", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file1": "", "file2": new Symlink("file1") } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir/file2"), callbackSpy.proxy); fs.writeFileSync("dir/file1", "test"); - callbackSpy.verify(_ => _("change", "file2"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["change", "file2"] + ]); }); it("unlink() (single link)", () => { const fs = new FileSystem(ignoreCase, { files: { [root]: { "dir": { "file1": "", "file2": new Symlink("file1") } } } }); - const callbackSpy = spy<(eventType: string, filename: string) => void>(); + const callbackSpy = createSpy(); fs.watch(vpath.combine(root, "dir/file2"), callbackSpy.proxy); fs.unlinkSync(vpath.combine(root, "dir/file1")); - callbackSpy.verify(_ => _("rename", "file2"), Times.once()); - callbackSpy.verify(_ => _(Arg.any(), Arg.any()), Times.once()); + assert.sameDeepMembers(callbackSpy.calls, [ + ["rename", "file2"] + ]); }); }); it("fail: invalid path (ENOENT)", () => { diff --git a/scripts/vfs/src/tests/utils.ts b/scripts/vfs/src/tests/utils.ts index 532c68cd829..1ac906f3892 100644 --- a/scripts/vfs/src/tests/utils.ts +++ b/scripts/vfs/src/tests/utils.ts @@ -11,4 +11,10 @@ export function theory(name: string, data: (Theory | any[])[], callback: (...arg it(title, () => callback(...args)); } }); +} + +export function createSpy() { + const calls: any[][] = []; + const proxy = function(...args: any[]): any { calls.push(args); } + return { proxy, calls }; } \ No newline at end of file diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 23b34109788..580a23f22c4 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -1,145 +1,64 @@ /// +/// /// /// -/// // NOTE: The contents of this file are all exported from the namespace 'fakes'. This is to // support the eventual conversion of harness into a modular system. // harness fakes namespace fakes { - export interface FakeServerHostOptions { - /** - * The `VirtualFleSystem` to use. If not specified, a new case-sensitive `VirtualFileSystem` - * is created. - */ - vfs?: vfs.FileSystem | { currentDirectory?: string, useCaseSensitiveFileNames?: boolean }; + const processExitSentinel = new Error("System exit"); + + export interface ServerHostOptions { /** * The virtual path to tsc.js. If not specified, a default of `"/.ts/tsc.js"` is used. */ executingFilePath?: string; - /** - * The new-line style. If not specified, a default of `"\n"` is used. - */ newLine?: "\r\n" | "\n"; - /** - * Indicates whether to include _safeList.json_. - */ safeList?: boolean; - /** - * Indicates whether to include a bare _lib.d.ts_. - */ lib?: boolean; - /** - * Indicates whether to use DOS paths by default. - */ dos?: boolean; } - export class FakeServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost, ts.ModuleResolutionHost { - public static readonly dosExecutingFilePath = vpath.combine(vfsutils.dosBuiltFolder, "tsc.js"); - public static readonly defaultExecutingFilePath = vpath.combine(vfsutils.builtFolder, "tsc.js"); - public static readonly dosDefaultCurrentDirectory = "c:/"; - public static readonly defaultCurrentDirectory = "/"; - public static readonly dosSafeListPath = "c:/safelist.json"; - 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 dosLibPath = vpath.combine(vfsutils.dosBuiltFolder, "lib.d.ts"); - public static readonly libPath = vpath.combine(vfsutils.builtFolder, "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(); + export class ServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost { public readonly vfs: vfs.FileSystem; public exitCode: number; - public watchFiles = true; - public watchDirectories = true; - - private static readonly processExitSentinel = new Error("System exit"); private readonly _output: string[] = []; - private readonly _trace: string[] = []; private readonly _executingFilePath: string; private readonly _getCanonicalFileName: (file: string) => string; - private _screenClears = 0; - private _watchedFiles: core.SortedMap | undefined; - private _watchedFilesSet: core.SortedSet | undefined; - private _watchedRecursiveDirectories: core.SortedMap | undefined; - private _watchedRecursiveDirectoriesSet: core.SortedSet | undefined; - private _watchedNonRecursiveDirectories: core.SortedMap | undefined; - private _watchedNonRecursiveDirectoriesSet: core.SortedSet | undefined; - constructor(options: FakeServerHostOptions = {}, files?: vfs.FileSet) { + constructor(vfs: vfs.FileSystem, options: ServerHostOptions = {}) { const { dos = false, - vfs: _vfs = {}, executingFilePath = dos - ? FakeServerHost.dosExecutingFilePath - : FakeServerHost.defaultExecutingFilePath, + ? vfsutils.dosTscPath + : vfsutils.tscPath, newLine = "\n", safeList = false, lib = false, } = options; - const currentDirectory = _vfs instanceof vfs.FileSystem ? _vfs.cwd() : - _vfs.currentDirectory !== undefined ? _vfs.currentDirectory : - dos ? FakeServerHost.dosDefaultCurrentDirectory : - FakeServerHost.defaultCurrentDirectory; - - const useCaseSensitiveFileNames = _vfs instanceof vfs.FileSystem ? !_vfs.ignoreCase : - _vfs.useCaseSensitiveFileNames !== undefined ? _vfs.useCaseSensitiveFileNames : - false; - - this.vfs = _vfs instanceof vfs.FileSystem - ? _vfs - : new vfs.FileSystem(!useCaseSensitiveFileNames, { cwd: currentDirectory }); - - if (this.vfs.isReadonly) { - this.vfs = this.vfs.shadow(); - } - - this.vfs.mkdirpSync(currentDirectory); - this.vfs.chdir(currentDirectory); - + this.vfs = vfs.isReadonly ? vfs.shadow() : vfs; this.useCaseSensitiveFileNames = !this.vfs.ignoreCase; this.newLine = newLine; this._executingFilePath = executingFilePath; this._getCanonicalFileName = ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames); - if (files) { - this.vfs.apply(files); - } - if (safeList) { - const safeListPath = dos ? FakeServerHost.dosSafeListPath : FakeServerHost.safeListPath; - this.vfs.mkdirpSync(vpath.dirname(safeListPath)); - this.vfs.writeFileSync(safeListPath, FakeServerHost.safeListContent); + const safelistPath = dos ? vfsutils.dosSafelistPath : vfsutils.safelistPath; + this.vfs.mkdirpSync(vpath.dirname(safelistPath)); + this.vfs.writeFileSync(safelistPath, vfsutils.safelistContent); } if (lib) { - const libPath = dos ? FakeServerHost.dosLibPath : FakeServerHost.libPath; + const libPath = dos ? vfsutils.dosLibPath : vfsutils.libPath; this.vfs.mkdirpSync(vpath.dirname(libPath)); - this.vfs.writeFileSync(libPath, FakeServerHost.libContent); + this.vfs.writeFileSync(libPath, vfsutils.emptyLibContent); } } - // #region DirectoryStructureHost members + // #region System members public readonly newLine: string; public readonly useCaseSensitiveFileNames: boolean; @@ -183,17 +102,9 @@ namespace fakes { public exit(exitCode?: number) { this.exitCode = exitCode; - throw FakeServerHost.processExitSentinel; + throw processExitSentinel; } - // #endregion DirectoryStructureHost members - // #region ModuleResolutionHost members - public trace(message: string) { - this._trace.push(message); - } - // #endregion - - // #region System members public readonly args: string[] = []; public getFileSize(path: string) { @@ -201,77 +112,11 @@ namespace fakes { } public watchFile(path: string, cb: ts.FileWatcherCallback) { - if (!this._watchedFiles) this._watchedFiles = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); - if (!this._watchedFilesSet) this._watchedFilesSet = new core.SortedSet(this.vfs.stringComparer); - - const previousCount = this._watchedFiles.get(path) || 0; - if (previousCount === 0) { - this._watchedFilesSet.add(path); - } - - this._watchedFiles.set(path, previousCount + 1); - - let watching = true; - const watcher = vfsutils.watchFile(this.vfs, path, (fileName, eventKind) => { - if (this.watchFiles) cb(fileName, eventKind); - }); - return { - close: () => { - if (watching) { - const previousCount = this._watchedFiles.get(path) || 0; - if (previousCount === 1) { - this._watchedFiles.delete(path); - this._watchedFilesSet.delete(path); - } - else { - this._watchedFiles.set(path, previousCount - 1); - } - watcher.close(); - watching = false; - } - } - }; + return vfsutils.watchFile(this.vfs, path, cb); } public watchDirectory(path: string, cb: ts.DirectoryWatcherCallback, recursive?: boolean): ts.FileWatcher { - const watchedDirectories = recursive - ? this._watchedRecursiveDirectories || (this._watchedRecursiveDirectories = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" })) - : this._watchedNonRecursiveDirectories || (this._watchedNonRecursiveDirectories = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" })); - - const watchedDirectoriesSet = recursive - ? this._watchedRecursiveDirectoriesSet || (this._watchedRecursiveDirectoriesSet = new core.SortedSet(this.vfs.stringComparer)) - : this._watchedNonRecursiveDirectoriesSet || (this._watchedNonRecursiveDirectoriesSet = new core.SortedSet(this.vfs.stringComparer)); - - const previousCount = watchedDirectories.get(path) || 0; - if (previousCount === 0) { - watchedDirectoriesSet.add(path); - } - - watchedDirectories.set(path, previousCount + 1); - - let watcher: ts.FileWatcher | undefined = vfsutils.watchDirectory(this.vfs, path, fileName => { - if (this.watchDirectories) { - cb(fileName); - } - }, recursive); - - return { - close: () => { - if (watcher) { - const previousCount = watchedDirectories.get(path) || 0; - if (previousCount === 1) { - watchedDirectories.delete(path); - watchedDirectoriesSet.delete(path); - } - else { - watchedDirectories.set(path, previousCount - 1); - } - - watcher.close(); - watcher = undefined; - } - } - }; + return vfsutils.watchDirectory(this.vfs, path, cb, recursive); } public resolvePath(path: string) { @@ -303,16 +148,12 @@ namespace fakes { return undefined; } - public setTimeout(callback: (...args: any[]) => void, timeout: number, ...args: any[]) { - return this.timers.setTimeout(callback, timeout, ...args); + public setTimeout(_callback: (...args: any[]) => void, _timeout: number, ..._args: any[]) { + return ts.notImplemented(); } - public clearTimeout(timeoutId: any): void { - this.timers.clearTimeout(timeoutId); - } - - public clearScreen(): void { - this._screenClears++; + public clearTimeout(_timeoutId: any): void { + return ts.notImplemented(); } // #endregion System members @@ -327,105 +168,14 @@ namespace fakes { // #endregion FormatDiagnosticsHost members // #region ServerHost members - public setImmediate(callback: (...args: any[]) => void, ...args: any[]): any { - return this.timers.setImmediate(callback, args); + public setImmediate(_callback: (...args: any[]) => void, ..._args: any[]): any { + return ts.notImplemented(); } - public clearImmediate(timeoutId: any): void { - this.timers.clearImmedate(timeoutId); + public clearImmediate(_timeoutId: any): void { + return ts.notImplemented(); } // #endregion ServerHost members - - public getOutput(): ReadonlyArray { - return this._output; - } - - public clearOutput() { - this._output.length = 0; - } - - public getTrace(): ReadonlyArray { - return this._trace; - } - - public clearTrace() { - this._trace.length = 0; - } - - // expectations - 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 runQueuedImmediateCallbacks() { - try { - this.timers.executeImmediates(); - } - catch (e) { - if (e !== FakeServerHost.processExitSentinel) { - throw e; - } - } - } - - public runQueuedTimeoutCallbacks() { - try { - this.timers.advanceToEnd(); - } - catch (e) { - if (e !== FakeServerHost.processExitSentinel) { - throw e; - } - } - } - - public checkOutputContains(expected: Iterable) { - const mapExpected = new Set(expected); - const mapSeen = new Set(); - for (const f of this._output) { - assert.isFalse(mapSeen.has(f), `Already found ${f} in ${JSON.stringify(this._output)}`); - if (mapExpected.has(f)) { - mapExpected.delete(f); - mapSeen.add(f); - } - } - assert.equal(mapExpected.size, 0, `Output is missing ${JSON.stringify(ts.flatMap(Array.from(mapExpected.keys()), key => key))} in ${JSON.stringify(this._output)}`); - } - - public checkOutputDoesNotContain(notExpected: Iterable) { - const mapExpectedToBeAbsent = new Set(notExpected); - for (const f of this._output) { - assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(this._output)}`); - } - } - - public checkWatchedFiles(expected: Iterable) { - return checkSortedSet(this._watchedFilesSet, expected); - } - - public checkWatchedDirectories(expected: Iterable, recursive = false) { - return checkSortedSet(recursive ? this._watchedRecursiveDirectoriesSet : this._watchedNonRecursiveDirectoriesSet, expected); - } - - public checkScreenClears(expected: number) { - assert.equal(this._screenClears, expected); - } } +} - function checkSortedSet(set: ReadonlySet | undefined, values: Iterable) { - const array = Array.from(values); - const size = set ? set.size : 0; - assert.strictEqual(size, array.length, `Actual: ${set ? Array.from(set) : []}, expected: ${array}.`); - if (set) { - for (const value of array) { - assert.isTrue(set.has(value)); - } - } - } -} \ No newline at end of file diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index fb792487085..3e6b4102342 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -73,10 +73,10 @@ "../services/codefixes/disableJsDiagnostics.ts", "harness.ts", - + + "assert.ts", "core.ts", "utils.ts", - "typemock.ts", "events.ts", "documents.ts", "vpath.ts", @@ -101,6 +101,7 @@ "./parallel/host.ts", "./parallel/worker.ts", "runner.ts", + "virtualFileSystemWithWatch.ts", "../server/protocol.ts", "../server/session.ts", "../server/client.ts", diff --git a/src/harness/typemock.ts b/src/harness/typemock.ts deleted file mode 100644 index c8609c125d2..00000000000 --- a/src/harness/typemock.ts +++ /dev/null @@ -1,41 +0,0 @@ -// NOTE: This namespace re-exports all of the exports from the typemock private package. - -// typemock library -namespace typemock { - const module = require("typemock"); - typemock.Arg = module.Arg; - typemock.Times = module.Times; - typemock.Mock = module.Mock; - typemock.Spy = module.Spy; - typemock.Inject = module.Inject; - typemock.Timers = module.Timers; - typemock.spy = module.spy; -} - -declare module "typemock_" { - import * as typemock_ from "typemock"; - global { - namespace typemock { - export import Arg = typemock_.Arg; - export import Times = typemock_.Times; - export import Mock = typemock_.Mock; - export import Spy = typemock_.Spy; - export import Returns = typemock_.Returns; - export import Throws = typemock_.Throws; - export import ThisArg = typemock_.ThisArg; - export import Callback = typemock_.Callback; - export import Fallback = typemock_.Fallback; - export import Setup = typemock_.Setup; - export import Callable = typemock_.Callable; - export import Constructable = typemock_.Constructable; - export import Inject = typemock_.Inject; - export import Timers = typemock_.Timers; - export import Timer = typemock_.Timer; - export import Timeout = typemock_.Timeout; - export import Interval = typemock_.Interval; - export import Immediate = typemock_.Immediate; - export import AnimationFrame = typemock_.AnimationFrame; - export import spy = typemock_.spy; - } - } -} \ No newline at end of file diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index 08d4eff8512..a64d7bdd2a5 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -1,10 +1,9 @@ /// /// /// -/// -/// namespace ts.projectSystem { + import CommandNames = server.CommandNames; const nullCancellationToken = server.nullCancellationToken; function createTestTypingsInstaller(host: server.ServerHost) { @@ -12,39 +11,26 @@ namespace ts.projectSystem { } describe("CompileOnSave affected list", () => { - interface ProjectFileList { - projectFileName: string; - files: string[]; - } + 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)); - function sendAffectedFileRequestAndCheckResult(session: server.Session, args: server.protocol.FileRequestArgs, expectedFileList: ProjectFileList | ProjectFileList[]) { - checkAffectedFiles(sendCompileOnSaveAffectedFileListRequest(session, args), expectedFileList); - } + assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`); - 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]; + for (let i = 0; i < actualResult.length; i++) { + const actualResultSingleProject = actualResult[i]; const expectedResultSingleProject = expectedFileList[i]; assert.equal(actualResultSingleProject.projectFileName, expectedResultSingleProject.projectFileName, `Actual result contains different projects than the expected result`); - const actualResultSingleProjectFileNameList = sort(actualResultSingleProject.fileNames, compareStringsCaseSensitive); - const expectedFiles = expectedResultSingleProject.files; - const expectedResultSingleProjectFileNameList = sort(expectedFiles, compareStringsCaseSensitive); + const actualResultSingleProjectFileNameList = actualResultSingleProject.fileNames.sort(); + const expectedResultSingleProjectFileNameList = map(expectedResultSingleProject.files, f => f.path).sort(); 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, @@ -61,30 +47,53 @@ namespace ts.projectSystem { } describe("for configured projects", () => { - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer2 = "/a/b/file1Consumer2.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer2]: `import {Foo} from "./moduleFile1"; let z = 10;`, - "/a/b/globalFile3.ts": `interface GlobalFoo { age: number }`, - "/a/b/moduleFile2.ts": `export var Foo4 = 10;` - }); - const session = createSession(host, createTestTypingsInstaller(host)); + 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; - openFilesForSession([moduleFile1, file1Consumer1], session); + beforeEach(() => { + moduleFile1 = { + path: "/a/b/moduleFile1.ts", + content: "export function Foo() { };" + }; - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + file1Consumer1 = { + path: "/a/b/file1Consumer1.ts", + content: `import {Foo} from "./moduleFile1"; export var y = 10;` + }; - sendChangeRequest(session, { - file: moduleFile1, + 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, { + file: moduleFile1.path, line: 1, offset: 1, endLine: 1, @@ -92,242 +101,9 @@ namespace ts.projectSystem { insertString: `export var T: number;` }); - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); - - // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };` - sendChangeRequest(session, { - file: moduleFile1, - line: 1, - offset: 46, - endLine: 1, - endOffset: 46, - insertString: `console.log('hi');` - }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1] }); - }); - - it("should be up-to-date with the reference map changes", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer2 = "/a/b/file1Consumer2.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer2]: `import {Foo} from "./moduleFile1"; let z = 10;`, - ["/a/b/globalFile3.ts"]: `interface GlobalFoo { age: number }`, - ["/a/b/moduleFile2.ts"]: `export var Foo4 = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); - - openFilesForSession([moduleFile1, file1Consumer1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); - - // Change file2 content to `let y = Foo();` - sendChangeRequest(session, { - file: file1Consumer1, - line: 1, - offset: 1, - endLine: 1, - endOffset: 28, - insertString: "" - }); - - sendChangeRequest(session, { - file: moduleFile1, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T: number;` - }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer2] }); - - // Add the import statements back to file2 - sendChangeRequest(session, { - file: file1Consumer1, - 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, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T2: string;` - }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); - }); - - it("should be up-to-date with changes made in non-open files", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer2 = "/a/b/file1Consumer2.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer2]: `import {Foo} from "./moduleFile1"; let z = 10;`, - "/a/b/globalFile3.ts": `interface GlobalFoo { age: number }`, - "/a/b/moduleFile2.ts": `export var Foo4 = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); - - openFilesForSession([moduleFile1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); - - host.vfs.writeFileSync(file1Consumer1, `let y = 10;`); - - sendChangeRequest(session, { - file: moduleFile1, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T: number;` - }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer2] }); - }); - - it("should be up-to-date with deleted files", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer2 = "/a/b/file1Consumer2.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer2]: `import {Foo} from "./moduleFile1"; let z = 10;`, - "/a/b/globalFile3.ts": `interface GlobalFoo { age: number }`, - "/a/b/moduleFile2.ts": `export var Foo4 = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); - - openFilesForSession([moduleFile1], session); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); - - sendChangeRequest(session, { - file: moduleFile1, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T: number;` - }); - - host.vfs.unlinkSync(file1Consumer2); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1] }); - }); - - it("should be up-to-date with newly created files", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer2 = "/a/b/file1Consumer2.ts"; - const file1Consumer3 = "/a/b/file1Consumer3.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer2]: `import {Foo} from "./moduleFile1"; let z = 10;`, - "/a/b/globalFile3.ts": `interface GlobalFoo { age: number }`, - "/a/b/moduleFile2.ts": `export var Foo4 = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); - - openFilesForSession([moduleFile1], session); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); - - host.vfs.writeFileSync(file1Consumer3, `import {Foo} from "./moduleFile1"; let y = Foo();`); - - sendChangeRequest(session, { - file: moduleFile1, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T: number;` - }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }); - }); - - it("should detect changes in non-root files", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ "compileOnSave": true, "files": ["${file1Consumer1}"] }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; let y = Foo();`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); - - openFilesForSession([moduleFile1, file1Consumer1], session); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1] }); - - // change file1 shape now, and verify both files are affected - sendChangeRequest(session, { - file: moduleFile1, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T: number;` - }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1] }); - - // change file1 internal, and verify only file1 is affected - sendChangeRequest(session, { - file: moduleFile1, + // Change the content of file1 to `export var T: number;export function Foo() { };` + changeModuleFile1InternalRequest1 = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, line: 1, offset: 1, endLine: 1, @@ -335,33 +111,173 @@ namespace ts.projectSystem { insertString: `var T1: number;` }); - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1] }); + 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 = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + 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 = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + 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 = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + 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] }]); + + file1Consumer1.content = `let y = 10;`; + host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); + + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); + }); + + it("should be up-to-date with deleted files", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + 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); + // Delete file1Consumer2 + host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); + }); + + it("should be up-to-date with newly created files", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + 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.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + 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 = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + 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] }]); }); it("should return all files if a global file changed shape", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer2 = "/a/b/file1Consumer2.ts"; - const globalFile3 = "/a/b/globalFile3.ts"; - const moduleFile2 = "/a/b/moduleFile2.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer2]: `import {Foo} from "./moduleFile1"; let z = 10;`, - [globalFile3]: `interface GlobalFoo { age: number }`, - [moduleFile2]: `export var Foo4 = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([globalFile3], session); - - // check after file1 shape changes - sendChangeRequest(session, { - file: globalFile3, + const changeGlobalFile3ShapeRequest = makeSessionRequest(CommandNames.Change, { + file: globalFile3.path, line: 1, offset: 1, endLine: 1, @@ -369,252 +285,225 @@ namespace ts.projectSystem { insertString: `var T2: string;` }); - sendAffectedFileRequestAndCheckResult(session, - { file: globalFile3 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }); + // 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] }]); }); it("should return empty array if CompileOnSave is not enabled", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{}`, - [moduleFile1]: `export function Foo() { };`, - "/a/b/file1Consumer1.ts": `import {Foo} from "./moduleFile1"; export var y = 10;`, - "/a/b/file1Consumer2.ts": `import {Foo} from "./moduleFile1"; let z = 10;`, - "/a/b/globalFile3.ts": `interface GlobalFoo { age: number }`, - "/a/b/moduleFile2.ts": `export var Foo4 = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([moduleFile1], session); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - []); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); }); it("should return empty array if noEmit is set", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ "compileOnSave": true, "compilerOptions": { "noEmit": true } }`, - [moduleFile1]: `export function Foo() { };`, - "/a/b/file1Consumer1.ts": `import {Foo} from "./moduleFile1"; export var y = 10;`, - "/a/b/file1Consumer2.ts": `import {Foo} from "./moduleFile1"; let z = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compileOnSave": true, + "compilerOptions": { + "noEmit": true + } + }` + }; + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([moduleFile1], session); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - []); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); }); it("should save when compileOnSave is enabled in base tsconfig.json", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer2 = "/a/b/file1Consumer2.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ "extends": "/a/tsconfig.json" }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer2]: `import {Foo} from "./moduleFile1"; let z = 10;`, - "/a/b/globalFile3.ts": `interface GlobalFoo { age: number }`, - "/a/b/moduleFile2.ts": `export var Foo4 = 10;`, - "/a/tsconfig.json": `{ "compileOnSave": true }`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "extends": "/a/tsconfig.json" + }` + }; + + const configFile2: FileOrFolder = { + path: "/a/tsconfig.json", + content: `{ + "compileOnSave": true + }` + }; + + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([moduleFile1, file1Consumer1], session); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer2] }); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); }); it("should always return the file itself if '--isolatedModules' is specified", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ "compileOnSave": true, "compilerOptions": { "isolatedModules": true } }`, - [moduleFile1]: `export function Foo() { };`, - "/a/b/file1Consumer1.ts": `import {Foo} from "./moduleFile1"; export var y = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compileOnSave": true, + "compilerOptions": { + "isolatedModules": true + } + }` + }; + const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([moduleFile1], session); - sendChangeRequest(session, { - file: moduleFile1, + const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, line: 1, offset: 27, endLine: 1, endOffset: 27, insertString: `Point,` }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1] }); + session.executeCommand(file1ChangeShapeRequest); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); }); it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ "compileOnSave": true, "compilerOptions": { "module": "system", "outFile": "/a/b/out.js" } }`, - [moduleFile1]: `export function Foo() { };`, - "/a/b/file1Consumer1.ts": `import {Foo} from "./moduleFile1"; export var y = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compileOnSave": true, + "compilerOptions": { + "module": "system", + "outFile": "/a/b/out.js" + } + }` + }; + const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([moduleFile1], session); - sendChangeRequest(session, { - file: moduleFile1, + const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, line: 1, offset: 27, endLine: 1, endOffset: 27, insertString: `Point,` }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1] }); + session.executeCommand(file1ChangeShapeRequest); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); }); it("should return cascaded affected file list", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const file1Consumer1 = "/a/b/file1Consumer1.ts"; - const file1Consumer1Consumer1 = "/a/b/file1Consumer1Consumer1.ts"; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [file1Consumer1]: `import {Foo} from "./moduleFile1"; export var y = 10;`, - [file1Consumer1Consumer1]: `import {y} from "./file1Consumer1";`, - "/a/b/globalFile3.ts": `interface GlobalFoo { age: number }`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + const file1Consumer1Consumer1: FileOrFolder = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([moduleFile1, file1Consumer1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }); - - sendChangeRequest(session, { - file: moduleFile1, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T: number;` - }); - - sendChangeRequest(session, { - file: file1Consumer1, + const changeFile1Consumer1ShapeRequest = makeSessionRequest(CommandNames.Change, { + file: file1Consumer1.path, line: 2, offset: 1, endLine: 2, endOffset: 1, insertString: `export var T: number;` }); - - sendAffectedFileRequestAndCheckResult(session, - { projectFileName: configFile, file: moduleFile1 }, - { projectFileName: configFile, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }); + session.executeCommand(changeModuleFile1ShapeRequest1); + session.executeCommand(changeFile1Consumer1ShapeRequest); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); }); it("should work fine for files with circular references", () => { - const configFile = "/a/b/tsconfig.json"; - const file1 = "/a/b/file1.ts"; - const file2 = "/a/b/file2.ts"; - const host = new fakes.FakeServerHost(); - host.vfs.apply({ - [configFile]: `{ compileOnSave": true }`, - [file1]: `/// \nexport var t1 = 10;`, - [file2]: `/// \nexport var t2 = 10;`, - }); - const session = createSession(host, createTestTypingsInstaller(host)); + 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 = createServerHost([file1, file2, configFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); openFilesForSession([file1, file2], session); - - sendAffectedFileRequestAndCheckResult(session, - { file: file1 }, - { projectFileName: configFile, files: [file1, file2] }); + const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); + sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); }); it("should return results for all projects if not specifying projectFileName", () => { - const file1 = "/a/b/file1.ts"; - const file2 = "/a/b/file2.ts"; - const file3 = "/a/c/file2.ts"; - const configFile1 = "/a/b/tsconfig.json"; - const configFile2 = "/a/c/tsconfig.json"; - const host = new fakes.FakeServerHost(); - host.vfs.apply({ - [file1]: `export var t = 10;`, - [file2]: `import {t} from "./file1"; var t2 = 11;`, - [file3]: `import {t} from "../b/file1"; var t3 = 11;`, - [configFile1]: `{ "compileOnSave": true }`, - [configFile2]: `{ "compileOnSave": true }`, - }); + 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 = createServerHost([file1, file2, file3, configFile1, configFile2]); const session = createSession(host); openFilesForSession([file1, file2, file3], session); + const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - sendAffectedFileRequestAndCheckResult(session, - { file: file1 }, - [ - { projectFileName: configFile1, files: [file1, file2] }, - { projectFileName: configFile2, files: [file1, file3] } - ]); + sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ + { projectFileName: configFile1.path, files: [file1, file2] }, + { projectFileName: configFile2.path, files: [file1, file3] } + ]); }); it("should detect removed code file", () => { - const configFile = "/a/b/tsconfig.json"; - const moduleFile1 = "/a/b/moduleFile1.ts"; - const referenceFile1 = "/a/b/referenceFile1.ts"; - const host = new fakes.FakeServerHost(); - host.vfs.apply({ - [configFile]: `{ compileOnSave": true }`, - [moduleFile1]: `export function Foo() { };`, - [referenceFile1]: `/// \nexport var x = Foo();`, - }); + const referenceFile1: FileOrFolder = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const host = createServerHost([moduleFile1, referenceFile1, configFile]); const session = createSession(host); openFilesForSession([referenceFile1], session); + host.reloadFS([referenceFile1, configFile]); - host.vfs.unlinkSync(moduleFile1); - - sendAffectedFileRequestAndCheckResult(session, - { file: referenceFile1 }, - { projectFileName: configFile, files: [referenceFile1] }); - - sendAffectedFileRequestAndCheckResult(session, - { file: moduleFile1 }, - []); + const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); + sendAffectedFileRequestAndCheckResult(session, request, [ + { projectFileName: configFile.path, files: [referenceFile1] } + ]); + const requestForMissingFile = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); + sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); }); it("should detect non-existing code file", () => { - const configFile = "/a/b/tsconfig.json"; - const referenceFile1 = "/a/b/referenceFile1.ts"; - const host = new fakes.FakeServerHost(); - host.vfs.apply({ - [configFile]: `{ compileOnSave": true }`, - [referenceFile1]: `/// \nexport var x = Foo();`, - }); + const referenceFile1: FileOrFolder = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const host = createServerHost([referenceFile1, configFile]); const session = createSession(host); openFilesForSession([referenceFile1], session); - - sendAffectedFileRequestAndCheckResult(session, - { file: referenceFile1 }, - { projectFileName: configFile, files: [referenceFile1] }); + const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); + sendAffectedFileRequestAndCheckResult(session, request, [ + { projectFileName: configFile.path, files: [referenceFile1] } + ]); }); }); }); @@ -624,39 +513,54 @@ namespace ts.projectSystem { test("\n"); test("\r\n"); - function test(newLine: "\r\n" | "\n") { - const f = "/a/app.ts"; - const content = `var x = 1;${newLine}var y = 2;`; - const host = new fakes.FakeServerHost({ newLine }, /*files*/ { - [f]: content - }); + 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 = createServerHost([f], { newLine }); const session = createSession(host); - - sendOpenRequest(session, { file: f }, 1); - sendCompileOnSaveEmitFileRequest(session, { file: f }, 2); - - const emitOutput = host.readFile("/a/app.js"); - assert.equal(emitOutput, content + newLine, "content of emit output should be identical with the input + newline"); + 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); + assert.equal(emitOutput, f.content + newLine, "content of emit output should be identical with the input + newline"); } }); it("should emit specified file", () => { - const file1 = "/a/b/f1.ts"; - const file2 = "/a/b/f2.ts"; - const configFile = "/a/b/tsconfig.json"; - const host = new fakes.FakeServerHost({ lib: true, newLine: "\r\n" }); - host.vfs.apply({ - [file1]: `export function Foo() { return 10; }`, - [file2]: `import {Foo} from "./f1"; let y = Foo();`, - [configFile]: `{}`, - }); - + 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 = createServerHost([file1, file2, configFile, libFile], { newLine: "\r\n" }); const typingsInstaller = createTestTypingsInstaller(host); const session = createSession(host, { typingsInstaller }); openFilesForSession([file1, file2], session); - - sendCompileOnSaveEmitFileRequest(session, { file: file1, projectFileName: configFile }); + const compileFileRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path }); + session.executeCommand(compileFileRequest); const expectedEmittedFileName = "/a/b/f1.js"; assert.isTrue(host.fileExists(expectedEmittedFileName)); @@ -664,25 +568,26 @@ 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 file1 = "/a/b/file1.ts"; - const file2 = "/a/b/file2.js"; - const file3 = "/a/b/file3.js"; - const file1Content = `consonle.log('file1');`; - const file2Content = `console.log'file2');`; - const file3Content = `console.log('file3');`; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [file1]: file1Content, - // file2 has errors. The emit should not be blocked. - [file2]: file2Content, - [file3]: file3Content, - }); - + const host = createServerHost([file1, file2, file3, libFile]); const session = createSession(host); const projectService = session.getProjectService(); projectService.openExternalProject({ - rootFiles: toExternalFiles([file1, file2]), + rootFiles: toExternalFiles([file1.path, file2.path]), options: { allowJs: true, outFile: "dist.js", @@ -691,30 +596,31 @@ namespace ts.projectSystem { projectFileName: externalProjectName }); - sendCompileOnSaveEmitFileRequest(session, { file: file1 }); + const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); + session.executeCommand(emitRequest); const expectedOutFileName = "/a/b/dist.js"; assert.isTrue(host.fileExists(expectedOutFileName)); const outFileContent = host.readFile(expectedOutFileName); - assert.isTrue(outFileContent.indexOf(file1Content) !== -1); - assert.isTrue(outFileContent.indexOf(file2Content) === -1); - assert.isTrue(outFileContent.indexOf(file3Content) === -1); + assert.isTrue(outFileContent.indexOf(file1.content) !== -1); + assert.isTrue(outFileContent.indexOf(file2.content) === -1); + assert.isTrue(outFileContent.indexOf(file3.content) === -1); }); 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 file1 = "/root/TypeScriptProject3/TypeScriptProject3/Foo.ts"; - const file1Content = `consonle.log('file1');`; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - [file1]: file1Content, - }); - + const host = createServerHost([file1, libFile]); const session = createSession(host); const projectService = session.getProjectService(); const outFileName = "bar.js"; projectService.openExternalProject({ - rootFiles: toExternalFiles([file1]), + rootFiles: toExternalFiles([file1.path]), options: { outFile: outFileName, sourceMap: true, @@ -723,20 +629,21 @@ namespace ts.projectSystem { projectFileName: externalProjectName }); - sendCompileOnSaveEmitFileRequest(session, { file: file1 }); + const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); + session.executeCommand(emitRequest); // Verify js file const expectedOutFileName = "/root/TypeScriptProject3/TypeScriptProject3/" + outFileName; assert.isTrue(host.fileExists(expectedOutFileName)); const outFileContent = host.readFile(expectedOutFileName); - verifyContentHasString(outFileContent, file1Content); + verifyContentHasString(outFileContent, file1.content); verifyContentHasString(outFileContent, `//# ${"sourceMappingURL"}=${outFileName}.map`); // Sometimes tools can sometimes see this line as a source mapping url comment, so we obfuscate it a little // Verify map file const expectedMapFileName = expectedOutFileName + ".map"; assert.isTrue(host.fileExists(expectedMapFileName)); const mapFileContent = host.readFile(expectedMapFileName); - verifyContentHasString(mapFileContent, `"sources":["Foo.ts"]`); + verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); function verifyContentHasString(content: string, str: string) { assert.isTrue(stringContains(content, str), `Expected "${content}" to have "${str}"`); diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index 1ec33ffd252..d073c4aa07b 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -111,7 +111,7 @@ namespace ts { function runBaseline(extension: Extension) { const path = "/a" + extension; - const program = makeProgram({ path, content: t.source }, includeLib); + const program = makeProgram(path, t.source, includeLib); if (hasSyntacticDiagnostics(program)) { // Don't bother generating JS baselines for inputs that aren't valid JS. @@ -146,18 +146,19 @@ namespace ts { const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); data.push(newTextWithRename); - const diagProgram = makeProgram({ path, content: newText }, includeLib); + const diagProgram = makeProgram(path, newText, includeLib); assert.isFalse(hasSyntacticDiagnostics(diagProgram)); } return data.join(newLineCharacter); }); } - function makeProgram(f: {path: string, content: string }, includeLib?: boolean) { + function makeProgram(path: string, content: string, includeLib?: boolean) { // libFile is expensive to parse repeatedly - only test when required - const host = new fakes.FakeServerHost({ lib: includeLib }, /*files*/ { [f.path]: f.content }); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { files: { [path]: content } }); + const host = new fakes.ServerHost(fs, { lib: includeLib }); const projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); + projectService.openClientFile(path); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); return program; } @@ -175,7 +176,8 @@ namespace ts { if (!selectionRange) { throw new Error(`Test ${caption} does not specify selection range`); } - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { "/a.ts": t.source }); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { files: { "/a.ts": t.source } }); + const host = new fakes.ServerHost(fs, { lib: true }); const projectService = projectSystem.createProjectService(host); projectService.openClientFile("/a.ts"); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index b35fde0aab1..dae465a3ef3 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -1,7 +1,6 @@ /// /// /// -/// namespace ts.projectSystem { describe("Project errors", () => { @@ -31,131 +30,177 @@ namespace ts.projectSystem { } it("external project - diagnostics for missing files", () => { - const host = new fakes.FakeServerHost({ safeList: true, lib: true }, /*files*/ { - "/a/b/app.ts": ``, - }); - - const projectFileName = "/a/b/test.csproj"; - + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/applib.ts", + content: "" + }; + const host = createServerHost([file1, libFile]); const session = createSession(host); const projectService = session.getProjectService(); - projectService.openExternalProject({ - projectFileName, - options: {}, - rootFiles: toExternalFiles(["/a/b/app.ts", "/a/b/applib.ts"]) - }); + const projectFileName = "/a/b/test.csproj"; + const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }; - // 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."]); + { + projectService.openExternalProject({ + projectFileName, + options: {}, + rootFiles: toExternalFiles([file1.path, file2.path]) + }); - host.vfs.unlinkSync("/a/b/app.ts"); - host.vfs.writeFileSync("/a/b/applib.ts", ``); + 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."]); + } - // 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.writeFileSync("/a/b/app.ts", ``); - - // both files exist - expect no errors - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags3 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName }, /*seq*/ 2); - checkDiagnosticsWithLinePos(diags3, []); + 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, []); + } }); it("configured projects - diagnostics for missing files", () => { - const host = new fakes.FakeServerHost({ safeList: true, lib: true }, /*files*/ { - "/a/b/app.ts": ``, - "/a/b/tsconfig.json": `{ "files": ["app.ts", "applib.ts"] }`, - }); - + 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 session = createSession(host); const projectService = session.getProjectService(); - - openFilesForSession(["/a/b/app.ts"], session); - + openFilesForSession([file1], session); checkNumberOfProjects(projectService, { configuredProjects: 1 }); const project = configuredProjectAt(projectService, 0); - const diags1 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName: project.getProjectName() }, /*seq*/ 2); - checkDiagnosticsWithLinePos(diags1, ["File '/a/b/applib.ts' not found."]); + 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."]); - host.vfs.writeFileSync("/a/b/applib.ts", ``); + host.reloadFS([file1, file2, config, libFile]); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const diags2 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName: project.getProjectName() }, /*seq*/ 2); - checkDiagnosticsWithLinePos(diags2, []); + diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, []); }); it("configured projects - diagnostics for corrupted config 1", () => { - const host = new fakes.FakeServerHost({ safeList: true }, /*files*/ { - "/a/b/app.ts": ``, - "/a/b/lib.ts": ``, - "/a/b/tsconfig.json": ` "files": ["app.ts", "lib.ts"] }`, - }); - + 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 projectService = createProjectService(host); - 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"); - + 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); + } // fix config and trigger watcher - host.vfs.writeFileSync("/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, []); + 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, []); + } }); it("configured projects - diagnostics for corrupted config 2", () => { - const host = new fakes.FakeServerHost({ safeList: true }, /*files*/ { - "/a/b/app.ts": ``, - "/a/b/lib.ts": ``, - "/a/b/tsconfig.json": `{ "files": ["app.ts", "lib.ts"] }`, - }); - + 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 projectService = createProjectService(host); - 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, []); - + 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, []); + } // break config and trigger watcher - host.vfs.writeFileSync("/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"); + 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); + } }); }); } diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 9cbe00c946e..454e97b3133 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -1,6 +1,5 @@ /// /// -/// namespace ts { @@ -885,6 +884,10 @@ namespace ts { }); }); + type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + import createTestSystem = ts.TestFSWithWatch.createWatchedSystem; + import libFile = ts.TestFSWithWatch.libFile; + describe("isProgramUptoDate should return true when there is no change in compiler options and", () => { function verifyProgramIsUptoDate( program: Program, @@ -917,37 +920,48 @@ namespace ts { verifyProgramIsUptoDate(program, fileNames, options); } - function verifyProgram(vfs: vfs.FileSystem, rootFiles: string[], options: CompilerOptions, configFile: string) { - const system = new fakes.FakeServerHost({ vfs }); + function verifyProgram(files: FileOrFolder[], rootFiles: string[], options: CompilerOptions, configFile: string) { + const system = createTestSystem(files); verifyProgramWithoutConfigFile(system, rootFiles, options); verifyProgramWithConfigFile(system, configFile); } it("has empty options", () => { - const file1 = "/a/b/file1.ts"; - const file2 = "/a/b/file2.ts"; - const configFile = "/a/b/tsconfig.json"; - const fs = new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/", files: { - [file1]: "let x = 1", - [file2]: "let y = 1", - [configFile]: "{}", - [fakes.FakeServerHost.libPath]: fakes.FakeServerHost.libContent, - }}); - - verifyProgram(fs, [file1, file2], {}, configFile); + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: "let x = 1" + }; + const file2: FileOrFolder = { + path: "/a/b/file2.ts", + content: "let y = 1" + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + verifyProgram([file1, file2, libFile, configFile], [file1.path, file2.path], {}, configFile.path); }); it("has lib specified in the options", () => { - const app = "/src/app.ts"; - const configFile = "/src/tsconfig.json"; const compilerOptions: CompilerOptions = { lib: ["es5", "es2015.promise"] }; - const fs = new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/", files: { - [app]: "var x: Promise;", - [configFile]: JSON.stringify({ compilerOptions }), - "/compiler/lib.es5.d.ts": "declare const eval: any;", - "/compiler/lib.es2015.promise.d.ts": "declare class Promise {}", - }}); - verifyProgram(fs, [app], compilerOptions, configFile); + const app: FileOrFolder = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const configFile: FileOrFolder = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + const es5Lib: FileOrFolder = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const es2015Promise: FileOrFolder = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + + verifyProgram([app, configFile, es5Lib, es2015Promise], [app.path], compilerOptions, configFile.path); }); it("has paths specified in the options", () => { @@ -961,26 +975,31 @@ namespace ts { ] } }; - const app = "/src/packages/framework/app.ts"; - const configFile = "/src/tsconfig.json"; - const fs = new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/", files: { - [app]: - `import classC from "module1/lib/file1";\n` + - `import classD from "module3/file3";\n` + - `let x = new classC();\n` + - `let y = new classD();`, - "/src/packages/mail/data/module1/lib/file1.ts": - `import classC from "module2/file2";\n` + - `export default classC;`, - "/src/packages/mail/data/module1/lib/module2/file2.ts": - `class classC { method2() { return "hello"; } }\n` + - `export default classC;`, - "/src/packages/styles/module3/file3.ts": - `class classD { method() { return 10; } }\n` + - `export default classD;`, - [configFile]: JSON.stringify({ compilerOptions }), - }}); - verifyProgram(fs, [app], compilerOptions, configFile); + const app: FileOrFolder = { + path: "/src/packages/framework/app.ts", + content: 'import classc from "module1/lib/file1";\ + import classD from "module3/file3";\ + let x = new classc();\ + let y = new classD();' + }; + const module1: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: FileOrFolder = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const configFile: FileOrFolder = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + + verifyProgram([app, module1, module2, module3, libFile, configFile], [app.path], compilerOptions, configFile.path); }); it("has include paths specified in tsconfig file", () => { @@ -994,27 +1013,30 @@ namespace ts { ] } }; - const configFile = "/src/tsconfig.json"; - const fs = new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/", files: { - "/src/packages/framework/app.ts": - `import classC from "module1/lib/file1";\n` + - `import classD from "module3/file3";\n` + - `let x = new classC();\n` + - `let y = new classD();`, - "/src/packages/mail/data/module1/lib/file1.ts": - `import classC from "module2/file2";\n` + - `export default classC;`, - "/src/packages/mail/data/module1/lib/module2/file2.ts": - `class classC { method2() { return "hello"; } }\n` + - `export default classC;`, - "/src/packages/styles/module3/file3.ts": - `class classD { method() { return 10; } }\n` + - `export default classD;`, - [configFile]: - JSON.stringify({ compilerOptions, include: ["packages/**/*.ts"] }), - }}); - const watchingSystemHost = new fakes.FakeServerHost({ vfs: fs }); - verifyProgramWithConfigFile(watchingSystemHost, configFile); + const app: FileOrFolder = { + path: "/src/packages/framework/app.ts", + content: 'import classc from "module1/lib/file1";\ + import classD from "module3/file3";\ + let x = new classc();\ + let y = new classD();' + }; + const module1: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: FileOrFolder = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const configFile: FileOrFolder = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions, include: ["packages/**/*.ts"] }) + }; + verifyProgramWithConfigFile(createTestSystem([app, module1, module2, module3, libFile, configFile]), configFile.path); }); }); } diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index e97be3a8f14..5cea42efcf5 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -1,59 +1,46 @@ /// /// -/// namespace ts.projectSystem { describe("project telemetry", () => { it("does nothing for inferred project", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a.js": "", - }); - const et = new TestServerEventManager(host); - et.service.openClientFile("/a.js"); + const file = makeFile("/a.js"); + const et = new TestServerEventManager([file]); + et.service.openClientFile(file.path); et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent); }); it("only sends an event once", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a/a.ts": ``, - "/b.ts": ``, - "/a/tsconfig.json": `{}`, - }); + const file = makeFile("/a/a.ts"); + const file2 = makeFile("/b.ts"); + const tsconfig = makeFile("/a/tsconfig.json", {}); - const et = new TestServerEventManager(host); - et.service.openClientFile("/a/a.ts"); - et.assertProjectInfoTelemetryEvent({}, "/a/tsconfig.json"); + const et = new TestServerEventManager([file, file2, tsconfig]); + et.service.openClientFile(file.path); + et.assertProjectInfoTelemetryEvent({}, tsconfig.path); - et.service.closeClientFile("/a/a.ts"); + et.service.closeClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 1 }); - et.service.openClientFile("/b.ts"); + et.service.openClientFile(file2.path); checkNumberOfProjects(et.service, { inferredProjects: 1 }); et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent); - et.service.openClientFile("/a/a.ts"); + et.service.openClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 1, inferredProjects: 1 }); et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent); }); it("counts files by extension", () => { + const files = ["ts.ts", "tsx.tsx", "moo.ts", "dts.d.ts", "jsx.jsx", "js.js", "badExtension.badExtension"].map(f => makeFile(`/src/${f}`)); + const notIncludedFile = makeFile("/bin/ts.js"); const compilerOptions: ts.CompilerOptions = { allowJs: true }; - const host = new fakes.FakeServerHost({}, /*files*/ { - "/src/ts.ts": ``, - "/src/tsx.tsx": ``, - "/src/moo.ts": ``, - "/src/dts.d.ts": ``, - "/src/jsx.jsx": ``, - "/src/js.js": ``, - "/src/badExtension.badExtension": ``, - "/bin/ts.js": ``, - "/tsconfig.json": JSON.stringify({ compilerOptions, include: ["src"] }), - }); + const tsconfig = makeFile("/tsconfig.json", { compilerOptions, include: ["src"] }); - const et = new TestServerEventManager(host); - et.service.openClientFile("/src/ts.ts"); + const et = new TestServerEventManager([...files, notIncludedFile, tsconfig]); + et.service.openClientFile(files[0].path); et.assertProjectInfoTelemetryEvent({ fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions, @@ -62,11 +49,8 @@ namespace ts.projectSystem { }); it("works with external project", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a.ts": ``, - }); - - const et = new TestServerEventManager(host); + const file1 = makeFile("/a.ts"); + const et = new TestServerEventManager([file1]); const compilerOptions: ts.server.protocol.CompilerOptions = { strict: true }; const projectFileName = "/hunter2/foo.csproj"; @@ -75,7 +59,7 @@ namespace ts.projectSystem { // TODO: Apparently compilerOptions is mutated, so have to repeat it here! et.assertProjectInfoTelemetryEvent({ - projectId: core.sha1("/hunter2/foo.csproj"), + projectId: Harness.mockHash("/hunter2/foo.csproj"), compilerOptions: { strict: true }, compileOnSave: true, // These properties can't be present for an external project, so they are undefined instead of false. @@ -97,7 +81,7 @@ namespace ts.projectSystem { function open(): void { et.service.openExternalProject({ - rootFiles: toExternalFiles(["/a.ts"]), + rootFiles: toExternalFiles([file1.path]), options: compilerOptions, projectFileName, }); @@ -106,6 +90,8 @@ namespace ts.projectSystem { }); it("does not expose paths", () => { + const file = makeFile("/a.ts"); + const compilerOptions: ts.CompilerOptions = { project: "", outFile: "hunter2.js", @@ -160,14 +146,10 @@ namespace ts.projectSystem { lib: ["es6", "dom"], }; (compilerOptions as any).unknownCompilerOption = "hunter2"; // These are always ignored. + const tsconfig = makeFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] }); - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a.ts": ``, - "/tsconfig.json": JSON.stringify({ compilerOptions, files: ["/a.ts"] }), - }); - - const et = new TestServerEventManager(host); - et.service.openClientFile("/a.ts"); + const et = new TestServerEventManager([file, tsconfig]); + et.service.openClientFile(file.path); et.assertProjectInfoTelemetryEvent({ compilerOptions: safeCompilerOptions, @@ -176,20 +158,18 @@ namespace ts.projectSystem { }); it("sends telemetry for extends, files, include, exclude, and compileOnSave", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/hunter2/a.ts": ``, - "/tsconfig.json": JSON.stringify({ - compilerOptions: {}, - extends: "hunter2.json", - files: ["hunter2/a.ts"], - include: ["hunter2"], - exclude: ["hunter2"], - compileOnSave: true, - }) + const file = makeFile("/hunter2/a.ts"); + const tsconfig = makeFile("/tsconfig.json", { + compilerOptions: {}, + extends: "hunter2.json", + files: ["hunter2/a.ts"], + include: ["hunter2"], + exclude: ["hunter2"], + compileOnSave: true, }); - const et = new TestServerEventManager(host); - et.service.openClientFile("/hunter2/a.ts"); + const et = new TestServerEventManager([tsconfig, file]); + et.service.openClientFile(file.path); et.assertProjectInfoTelemetryEvent({ extends: true, files: true, @@ -209,22 +189,20 @@ namespace ts.projectSystem { }; it("sends telemetry for typeAcquisition settings", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a.js": ``, - "/jsconfig.json": JSON.stringify({ - compilerOptions: {}, - typeAcquisition: { - enable: true, - enableAutoDiscovery: false, - include: ["hunter2", "hunter3"], - exclude: [], - }, - }) + const file = makeFile("/a.js"); + const jsconfig = makeFile("/jsconfig.json", { + compilerOptions: {}, + typeAcquisition: { + enable: true, + enableAutoDiscovery: false, + include: ["hunter2", "hunter3"], + exclude: [], + }, }); - const et = new TestServerEventManager(host); - et.service.openClientFile("/a.js"); + const et = new TestServerEventManager([jsconfig, file]); + et.service.openClientFile(file.path); et.assertProjectInfoTelemetryEvent({ - projectId: core.sha1("/jsconfig.json"), + projectId: Harness.mockHash("/jsconfig.json"), fileStats: fileStats({ js: 1 }), compilerOptions: autoJsCompilerOptions, typeAcquisition: { @@ -237,17 +215,14 @@ namespace ts.projectSystem { }); it("detects whether language service was disabled", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a.js": ``, - "/jsconfig.json": `{}`, - }); - - const et = new TestServerEventManager(host); + const file = makeFile("/a.js"); + const tsconfig = makeFile("/jsconfig.json", {}); + const et = new TestServerEventManager([tsconfig, file]); et.host.getFileSize = () => server.maxProgramSizeForNonTsFiles + 1; - et.service.openClientFile("/a.js"); + et.service.openClientFile(file.path); et.getEvent(server.ProjectLanguageServiceStateEvent); et.assertProjectInfoTelemetryEvent({ - projectId: core.sha1("/jsconfig.json"), + projectId: Harness.mockHash("/jsconfig.json"), fileStats: fileStats({ js: 1 }), compilerOptions: autoJsCompilerOptions, configFileName: "jsconfig.json", @@ -260,4 +235,8 @@ namespace ts.projectSystem { }); }); }); + + function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder { + return { path, content: isString(content) ? "" : JSON.stringify(content) }; + } } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 185e0bb139a..b6d3099dc77 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1,16 +1,18 @@ -/// -/// -/// -/// -/// +/// +/// +/// namespace ts.tscWatch { - import theory = utils.theory; - import spy = typemock.spy; - import Arg = typemock.Arg; - import Times = typemock.Times; - import checkFileNames = utils.checkFileNames; + 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; + import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain; export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { checkFileNames(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); @@ -20,38 +22,57 @@ namespace ts.tscWatch { checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); } - function createWatchOfConfigFile(configFileName: string, host: ts.System, maxNumberOfFilesToIterateForInvalidation?: number) { + function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { const compilerHost = ts.createWatchCompilerHostOfConfigFile(configFileName, {}, host); compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; const watch = createWatchProgram(compilerHost); return () => watch.getCurrentProgram().getProgram(); } - function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: ts.System, options: CompilerOptions = {}) { + function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}) { const watch = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host)); return () => watch.getCurrentProgram().getProgram(); } - function formatOutputFile(path: string, host: ts.System) { - return `TSFILE: ${path}${host.newLine}`; + function getEmittedLineForMultiFileOutput(file: FileOrFolder, host: WatchedSystem) { + return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; } - 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); - } + function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { + 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); } } 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); + } + enum ExpectedOutputErrorsPosition { BeforeCompilationStarts, AfterCompilationStarting, @@ -59,7 +80,7 @@ namespace ts.tscWatch { } function checkOutputErrors( - host: fakes.FakeServerHost, + host: WatchedSystem, errors: ReadonlyArray, errorsPosition: ExpectedOutputErrorsPosition, skipWaiting?: true @@ -98,1494 +119,2028 @@ namespace ts.tscWatch { host.clearOutput(); } - function assertDiagnosticAt(host: fakes.FakeServerHost, outputAt: number, diagnostic: Diagnostic) { + function assertDiagnosticAt(host: WatchedSystem, outputAt: number, diagnostic: Diagnostic) { const output = host.getOutput()[outputAt]; assert.equal(output, formatDiagnostic(diagnostic, host), "outputs[" + outputAt + "] is " + output); } - function assertWatchDiagnosticAt(host: fakes.FakeServerHost, outputAt: number, diagnosticMessage: DiagnosticMessage) { + function assertWatchDiagnosticAt(host: WatchedSystem, outputAt: number, diagnosticMessage: DiagnosticMessage) { const output = host.getOutput()[outputAt]; assert.isTrue(endsWith(output, getWatchDiagnosticWithoutDate(host, diagnosticMessage)), "outputs[" + outputAt + "] is " + output); } - function getWatchDiagnosticWithoutDate(host: fakes.FakeServerHost, diagnosticMessage: DiagnosticMessage) { + function getWatchDiagnosticWithoutDate(host: WatchedSystem, diagnosticMessage: DiagnosticMessage) { return ` - ${flattenDiagnosticMessageText(getLocaleSpecificMessage(diagnosticMessage), host.newLine)}${host.newLine + host.newLine + host.newLine}`; } - 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); - } + function getDiagnosticOfFileFrom(file: SourceFile, text: string, start: number, length: number, message: DiagnosticMessage): Diagnostic { return { file, start, length, + messageText: text, category: message.category, code: message.code, }; } - function createCompilerDiagnostic(message: DiagnosticMessage, ...args: string[]): Diagnostic { - return createDiagnostic(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); + 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 createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ...args: string[]): Diagnostic { - return createDiagnostic(file, start, length, 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 createUnknownCompilerOptionDiagnostic(program: Program, content: string, option: string) { + function getUnknownCompilerOption(program: Program, configFile: FileOrFolder, option: string) { const quotedOption = `"${option}"`; - return createFileDiagnostic(getConfigFile(program), content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); + return getDiagnosticOfFile(program.getCompilerOptions().configFile, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); } - 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 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 createCannotFindModuleDiagnostic(program: Program, path: string, content: string, moduleName: string) { + function getDiagnosticModuleNotFoundOfFile(program: Program, file: FileOrFolder, moduleName: string) { const quotedModuleName = `"${moduleName}"`; - return createFileDiagnostic(getFile(program, path), content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); + return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); } - 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); - } - - 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); - } - - 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); - } - - describe("tsc-watch", () => { - - describe("program updates", () => { - it("create watch without config file", () => { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/c/app.ts": `import {f} from "./module"\nconsole.log(f)`, - "/a/b/c/module.d.ts": `export let x: number`, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/c/app.ts"], host); - checkProgramActualFiles(watch(), ["/a/b/c/app.ts", fakes.FakeServerHost.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 fakes.FakeServerHost({ vfs: { useCaseSensitiveFileNames: false } }, /*files*/ { - "/a/b/app.ts": `let x = 1`, - "/a/b/tsconfig.json": `{ "include": ["app.ts"] }`, - }); - - const watch = createWatchOfConfigFile("/A/B/tsconfig.json", host); - checkProgramActualFiles(watch(), ["/A/B/app.ts"]); - }); - - it("create configured project without file list", () => { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/tsconfig.json": `{ "compilerOptions": {}, "exclude": ["e"] }`, - "/a/b/c/f1.ts": `let x = 1`, - "/a/b/d/f2.ts": `let y = 1`, - "/a/b/e/f3.ts": `let z = 1`, - }); - - const watch = createWatchProgram(createWatchCompilerHostOfConfigFile("/a/b/tsconfig.json", {}, host, /*createProgram*/ undefined, notImplemented)); - - checkProgramActualFiles(watch.getCurrentProgram().getProgram(), ["/a/b/c/f1.ts", fakes.FakeServerHost.libPath, "/a/b/d/f2.ts"]); - checkProgramRootFiles(watch.getCurrentProgram().getProgram(), ["/a/b/c/f1.ts", "/a/b/d/f2.ts"]); - host.checkWatchedFiles(["/a/b/tsconfig.json", "/a/b/c/f1.ts", "/a/b/d/f2.ts", fakes.FakeServerHost.libPath]); - host.checkWatchedDirectories(["/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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/commonFile1.ts": `let x = 1`, - "/a/b/tsconfig.json": `{}`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - host.checkWatchedDirectories(["/a/b", "/a/b/node_modules/@types"], /*recursive*/ true); - checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); - - // add a new ts file - host.vfs.writeFileSync("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/commonFile1.ts": `let x = 1`, - "/a/b/commonFile2.ts": `let y = 1`, - "/a/b/tsconfig.json": `{ "compilerOptions": {}, "files": ["commonFile1.ts", "commonFile3.ts"] }`, - }); - - const watch = createWatchOfConfigFile("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/commonFile1.ts": `let x = 1`, - "/a/b/commonFile2.ts": `let y = 1`, - "/a/b/tsconfig.json": `{}`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); - - // delete commonFile2 - host.vfs.unlinkSync("/a/b/commonFile2.ts"); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); - - // re-add commonFile2 - host.vfs.writeFileSync("/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 fakes.FakeServerHost({ lib: true, vfs: { useCaseSensitiveFileNames: false } }, /*files*/ { - "/a/b/commonFile1.ts": file1Content, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/commonFile1.ts"], host); - - checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); - checkProgramActualFiles(watch(), ["/a/b/commonFile1.ts", fakes.FakeServerHost.libPath]); - checkOutputErrors(host, [ - createFileNotFoundDiagnostic(watch(), "/a/b/commonFile1.ts", file1Content, "commonFile2.ts", "/a/b/commonFile2.ts"), - createCannotFindNameDiagnostic(watch(), "/a/b/commonFile1.ts", file1Content, "y"), - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.vfs.writeFileSync("/a/b/commonFile2.ts", `let y = 1`); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); - checkProgramActualFiles(watch(), ["/a/b/commonFile1.ts", fakes.FakeServerHost.libPath, "/a/b/commonFile2.ts"]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - it("should reflect change in config file", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a/b/commonFile1.ts": `let x = 1`, - "/a/b/commonFile2.ts": `let y = 1`, - "/a/b/tsconfig.json": `{ "compilerOptions": {}, "files": ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"] }`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts", "/a/b/commonFile2.ts"]); - - host.vfs.writeFileSync("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/commonFile1.ts": `let x = 1`, - "/a/b/commonFile2.ts": `let y = 1`, - "/a/c/excludedFile1.ts": `let t = 1;`, - "/a/tsconfig.json": `{ "compilerOptions": {}, "exclude": ["/a/c"] }`, - }); - - const watch = createWatchOfConfigFile("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/file1.ts": `import { T } from "module1";`, - "/a/b/node_modules/module1.ts": `export interface T {}`, - "/a/module1.ts": `export interface T {}`, - "/a/b/tsconfig.json": `{ "compilerOptions": { "moduleResolution": "node" }, "files": ["/a/b/file1.ts"] }`, - }); - - const watch = createWatchOfConfigFile("/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.writeFileSync("/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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/commonFile1.ts": `let x = 1`, - "/a/b/commonFile2.ts": `let y = 1`, - "/a/b/tsconfig.json": - `{\n` + - ` "compilerOptions": {\n` + - ` "target": "es6",\n` + - ` "allowAnything": true\n` + - ` },\n` + - ` "someOtherProperty": {}\n` + - `}` - }); - - const watch = createWatchOfConfigFile("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": `export * from "./f2"`, - "/a/b/f2.ts": `export let x = 1`, - "/a/c/f3.ts": `export let y = 1;`, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/f1.ts"], host); - checkProgramRootFiles(watch(), ["/a/b/f1.ts"]); - checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); - - host.vfs.writeFileSync("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": `export * from "./f2"`, - "/a/b/f2.ts": `export * from "../c/f3"`, - "/a/c/f3.ts": `export let y = 1;`, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/f1.ts"], host); - checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts", "/a/c/f3.ts"]); - - host.vfs.unlinkSync("/a/b/f2.ts"); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramActualFiles(watch(), ["/a/b/f1.ts"]); - }); - - it("deleted files affect project structure - 2", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": `export * from "./f2"`, - "/a/b/f2.ts": `export * from "../c/f3"`, - "/a/c/f3.ts": `export let y = 1;`, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/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.unlinkSync("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": `export let x = 5`, - "/a/c/f2.ts": `import {x} from "../b/f1"`, - "/a/c/f3.ts": `export let y = 1`, - "/a/c/tsconfig.json": `{ "compilerOptions": {}, "files": ["f2.ts", "f3.ts"] }`, - }); - - const watch = createWatchOfConfigFile("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": - `export * from "../c/f2";\n` + - `export * from "../d/f3";`, - "/a/c/f2.ts": `export let x = 1;`, - "/a/d/f3.ts": `export let y = 1;`, - }); - - const watch1 = createWatchOfFilesAndCompilerOptions(["/a/c/f2.ts", "/a/d/f3.ts"], host); - checkProgramActualFiles(watch1(), ["/a/c/f2.ts", "/a/d/f3.ts"]); - - const watch2 = createWatchOfFilesAndCompilerOptions(["/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": `let x = 1`, - "/a/b/tsconfig.json": `{ "compilerOptions": {} }`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkProgramActualFiles(watch(), ["/a/b/f1.ts"]); - - host.vfs.writeFileSync("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": `let x = 1`, - "/a/b/f2.ts": `let y = 1`, - "/a/b/tsconfig.json": `{ "compilerOptions": {}, "files": ["f1.ts"] }`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkProgramActualFiles(watch(), ["/a/b/f1.ts"]); - - host.vfs.writeFileSync("/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 fakes.FakeServerHost({}, /*files*/ { - "/a/b/f1.ts": `let x = 1`, - "/a/b/f2.ts": `let y = 1`, - "/a/b/tsconfig.json": `{ "compilerOptions": {}, "files": ["f1.ts", "f2.ts"] }`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts"]); - - host.vfs.writeFileSync("/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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/f1.ts": `let x = 1`, - "/a/b/f2.ts": `let y = 1`, - "/a/b/tsconfig.json": `{ "compilerOptions": {} }`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - - checkProgramActualFiles(watch(), ["/a/b/f1.ts", "/a/b/f2.ts", fakes.FakeServerHost.libPath]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.vfs.unlinkSync("/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") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected, /*skipWaiting*/ true); - }); - - it("Proper errors: document is not contained in project", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a/b/app.ts": ``, - "/a/b/tsconfig.json": `{`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkProgramActualFiles(watch(), ["/a/b/app.ts"]); - }); - - it("correctly handles changes in lib section of config file", () => { - const es5LibPath = vpath.combine(vfsutils.builtFolder, "lib.es5.d.ts"); - const es2015PromiseLibPath = vpath.combine(vfsutils.builtFolder, "lib.es2015.promise.d.ts"); - const host = new fakes.FakeServerHost({}, /*files*/ { - [es5LibPath]: `declare const eval: any`, - [es2015PromiseLibPath]: `declare class Promise {}`, - "/src/app.ts": `var x: Promise;`, - "/src/tsconfig.json": `{ "compilerOptions": { "lib": ["es5"] } }`, - }); - - const watch = createWatchOfConfigFile("/src/tsconfig.json", host); - checkProgramActualFiles(watch(), [es5LibPath, "/src/app.ts"]); - - host.vfs.writeFileSync("/src/tsconfig.json", `{ "compilerOptions": { "lib": ["es5", "es2015.promise"] } }`); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramActualFiles(watch(), [es5LibPath, es2015PromiseLibPath, "/src/app.ts"]); - }); - - it("should handle non-existing directories in config file", () => { - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a/src/app.ts": `let x = 1;`, - "/a/tsconfig.json": `{ "compilerOptions": {}, "include": ["src/**/*", "notexistingfolder/*"] }`, - }); - - const watch = createWatchOfConfigFile("/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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/file1.ts": file1Content, - "/a/b/moduleFile.ts": `export function bar() { };`, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/file1.ts"], host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.vfs.unlinkSync("/a/b/moduleFile.js"); - host.vfs.renameSync("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/b/file1.ts", file1Content, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - host.vfs.renameSync("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - 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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/file1.ts": file1Content, - "/a/b/moduleFile.ts": `export function bar() { };`, - "/a/b/tsconfig.json": `{}`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.vfs.unlinkSync("/a/b/moduleFile.js"); - host.vfs.renameSync("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/b/file1.ts", file1Content, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - host.vfs.unlinkSync("/a/b/moduleFile1.js"); - host.vfs.renameSync("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - it("types should load from config file path if config exists", () => { - const host = new fakes.FakeServerHost({ vfs: { currentDirectory: "/a/c" } }, /*files*/ { - "/a/c": {}, - "/a/b/app.ts": `let x = 1`, - "/a/b/tsconfig.json": `{ "compilerOptions": { "types": ["node"], "typeRoots": [] } }`, - "/a/b/node_modules/@types/node/index.d.ts": `declare var process: any`, - }); - - const watch = createWatchOfConfigFile("/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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/file1.ts": file1Content, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/file1.ts"], host); - - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/b/file1.ts", file1Content, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.vfs.writeFileSync("/a/b/moduleFile.ts", `export function bar() { };`); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - it("Configure file diagnostics events are generated when the config file has errors", () => { - const configFileContent = `{ "compilerOptions": { "foo": "bar", "allowJS": true } }`; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/app.ts": `let x = 10`, - "/a/b/tsconfig.json": configFileContent, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkOutputErrors(host, [ - createUnknownCompilerOptionDiagnostic(watch(), configFileContent, "foo"), - createUnknownCompilerOptionDiagnostic(watch(), configFileContent, "allowJS") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.BeforeCompilationStarts); - }); - - it("If config file doesnt have errors, they are not reported", () => { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/app.ts": `let x = 10`, - "/a/b/tsconfig.json": `{ "compilerOptions": {} }`, - }); - - createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - }); - - it("Reports errors when the config file changes", () => { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/app.ts": `let x = 10`, - "/a/b/tsconfig.json": `{ "compilerOptions": {} }`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - const configFileBadContent = `{ "compilerOptions": { "haha": 123 } }`; - host.vfs.writeFileSync("/a/b/tsconfig.json", configFileBadContent); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ - createUnknownCompilerOptionDiagnostic(watch(), configFileBadContent, "haha") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - host.vfs.writeFileSync("/a/b/tsconfig.json", `{ "compilerOptions": {} }`); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/file1.ts": `let t = 10;`, - "/a/b/tsconfig.json": `{ "compilerOptions": {}, "include": ["app/*", "test/**/*", "something"] }`, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkProgramActualFiles(watch(), [fakes.FakeServerHost.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 fakes.FakeServerHost({ vfs: { currentDirectory: "/a/" } }, /*files*/ { - "/a/app.ts": `let x = 1`, - "/a/tsconfig.json": `{ "compilerOptions": {}, "files": [] }`, - "/a/node_modules/@types/typings/index.d.ts": `export * from "./lib"`, - "/a/node_modules/@types/typings/lib.d.ts": `export const x: number`, - }); - - const watch = createWatchOfConfigFile("/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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/compile": `let x = 1`, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/compile"], host, { allowNonTsExtensions: true }); - checkProgramActualFiles(watch(), ["/a/compile", fakes.FakeServerHost.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 fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/app.ts": `let x = 10`, - "/a/b/tsconfig.json": configFileContentWithComment, - }); - - const watch = createWatchOfConfigFile("/a/b/tsconfig.json", host); - const initialErrors = [ - createExclusiveCompilerOptionDiagnostic(watch(), configFileContentWithComment, "allowJs", "declaration", /*checkFirst*/ true), - createExclusiveCompilerOptionDiagnostic(watch(), configFileContentWithComment, "allowJs", "declaration", /*checkFirst*/ false) - ]; - checkOutputErrors(host, initialErrors, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.vfs.writeFileSync("/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, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - assert.equal(nowErrors[0].start, initialErrors[0].start - configFileContentComment.length); - assert.equal(nowErrors[1].start, initialErrors[1].start - configFileContentComment.length); - }); - - it("should not trigger recompilation because of program emit", () => { - const proj = "/user/username/projects/myproject"; - const host = new fakes.FakeServerHost({ lib: true, vfs: { currentDirectory: proj } }, /*files*/ { [proj]: { - "file1.ts": `export const c = 30;`, - "src/file2.ts": `import {c} from "file1"; export const d = 30;`, - "tsconfig.json": JSON.stringify({ - compilerOptions: { - module: "amd", - outDir: "build" - } - }) - }}); - const watch = createWatchOfConfigFile(`${proj}/tsconfig.json`, host, /*maxNumberOfFilesToIterateForInvalidation*/1); - checkProgramActualFiles(watch(), [`${proj}/file1.ts`, `${proj}/src/file2.ts`, fakes.FakeServerHost.libPath]); - - assert.isTrue(host.fileExists("build/file1.js")); - assert.isTrue(host.fileExists("build/src/file2.js")); - - // This should be 0 - host.checkTimeoutQueueLengthAndRun(0); - }); - - it("shouldnt report error about unused function incorrectly when file changes from global to module", () => { - const getFileContent = (asModule: boolean) => ` - function one() {} - ${asModule ? "export " : ""}function two() { - return function three() { - one(); - } - }`; - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/file.ts": getFileContent(/*asModule*/ false) - }); - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/file.ts"], host, { - noUnusedLocals: true - }); - checkProgramActualFiles(watch(), ["/a/b/file.ts", fakes.FakeServerHost.libPath]); - checkOutputErrors(host, [], ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.writeFile("/a/b/file.ts", getFileContent(/*asModule*/ true)); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), ["/a/b/file.ts", fakes.FakeServerHost.libPath]); - checkOutputErrors(host, [], ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); + 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" + }; + + it("create watch without config file", () => { + const appFile: FileOrFolder = { + path: "/a/b/c/app.ts", + content: ` + import {f} from "./module" + console.log(f) + ` + }; + + const moduleFile: FileOrFolder = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + const host = createWatchedSystem([appFile, moduleFile, libFile]); + const watch = createWatchOfFilesAndCompilerOptions([appFile.path], host); + + checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); + + // 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(libFile.path, moduleFile.path)); }); - describe("emit once", () => { - function verifyFilesEmittedOnce(useOutFile: boolean) { - const configContent = JSON.stringify({ + 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 = createWatchOfConfigFile(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 watch = createWatchProgram(createWatchCompilerHostOfConfigFile(configFile.path, {}, host, /*createProgram*/ undefined, notImplemented)); + + checkProgramActualFiles(watch.getCurrentProgram().getProgram(), [file1.path, libFile.path, file2.path]); + checkProgramRootFiles(watch.getCurrentProgram().getProgram(), [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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfFilesAndCompilerOptions([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") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + host.reloadFS([file1, commonFile2, libFile]); + host.runQueuedTimeoutCallbacks(); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + + 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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfFilesAndCompilerOptions([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 = createWatchOfFilesAndCompilerOptions([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 = createWatchOfFilesAndCompilerOptions([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 = createWatchOfConfigFile(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 = createWatchOfFilesAndCompilerOptions([file2.path, file3.path], host); + checkProgramActualFiles(watch(), [file2.path, file3.path]); + + const watch2 = createWatchOfFilesAndCompilerOptions([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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(config.path, host); + + checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + 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) + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected, /*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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfFilesAndCompilerOptions([file1.path], host); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + 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") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + + 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 = createWatchOfConfigFile(configFile.path, host); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + 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") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + + 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 = createWatchOfConfigFile(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 = createWatchOfFilesAndCompilerOptions([file1.path], host); + + checkOutputErrors(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + host.reloadFS([file1, moduleFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + + 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 = createWatchOfConfigFile(configFile.path, host); + checkOutputErrors(host, [ + getUnknownCompilerOption(watch(), configFile, "foo"), + getUnknownCompilerOption(watch(), configFile, "allowJS") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.BeforeCompilationStarts); + }); + + 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]); + createWatchOfConfigFile(configFile.path, host); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + }); + + 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 = createWatchOfConfigFile(configFile.path, host); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + configFile.content = `{ + "compilerOptions": { + "haha": 123 + } + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, [ + getUnknownCompilerOption(watch(), configFile, "haha") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + + configFile.content = `{ + "compilerOptions": {} + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + + 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 = createWatchOfConfigFile(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 = createWatchOfConfigFile(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 = createWatchOfFilesAndCompilerOptions([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 = createWatchOfConfigFile(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, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + configFile.content = configFileContentWithoutCommentLine; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + const nowErrors = errors(); + checkOutputErrors(host, nowErrors, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + assert.equal(nowErrors[0].start, intialErrors[0].start - configFileContentComment.length); + assert.equal(nowErrors[1].start, intialErrors[1].start - configFileContentComment.length); + }); + + it("should not trigger recompilation because of program emit", () => { + const proj = "/user/username/projects/myproject"; + const file1: FileOrFolder = { + path: `${proj}/file1.ts`, + content: "export const c = 30;" + }; + const file2: FileOrFolder = { + path: `${proj}/src/file2.ts`, + content: `import {c} from "file1"; export const d = 30;` + }; + const tsconfig: FileOrFolder = { + path: `${proj}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "amd", + outDir: "build" + } + }) + }; + const host = createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: proj }); + const watch = createWatchOfConfigFile(tsconfig.path, host, /*maxNumberOfFilesToIterateForInvalidation*/1); + checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); + + assert.isTrue(host.fileExists("build/file1.js")); + assert.isTrue(host.fileExists("build/src/file2.js")); + + // This should be 0 + host.checkTimeoutQueueLengthAndRun(0); + }); + + it("shouldnt report error about unused function incorrectly when file changes from global to module", () => { + const getFileContent = (asModule: boolean) => ` + function one() {} + ${asModule ? "export " : ""}function two() { + return function three() { + one(); + } + }`; + const file: FileOrFolder = { + path: "/a/b/file.ts", + content: getFileContent(/*asModule*/ false) + }; + const files = [file, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([file.path], host, { + noUnusedLocals: true + }); + checkProgramActualFiles(watch(), files.map(file => file.path)); + checkOutputErrors(host, [], ExpectedOutputErrorsPosition.AfterCompilationStarting); + + file.content = getFileContent(/*asModule*/ true); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), files.map(file => file.path)); + checkOutputErrors(host, [], ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + }); + + 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); + createWatchOfConfigFile(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({ compilerOptions: useOutFile ? { outFile: "../output/common.js", target: "es5" } : { outDir: "../output", target: "es5" }, - 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" - ] - }); - - const host = new fakes.FakeServerHost({}, /*files*/ { - "/a/b/output/AnotherDependency/file1.d.ts": `declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }`, - "/a/b/dependencies/file2.d.ts": `declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }`, - "/a/b/project/src/main.ts": `namespace Main { export function fooBar() { } }`, - "/a/b/project/src/main2.ts": `namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }`, - "/a/b/project/tsconfig.json": configContent, - }); - - const writeFileSpy = spy(host, "writeFile"); - - createWatchOfConfigFile("/a/b/project/tsconfig.json", host); - - if (useOutFile) { - writeFileSpy - .verify(_ => _("/a/b/output/common.js", Arg.string(), Arg.any()), Times.once()) - .verify(_ => _(Arg.string(), Arg.string(), Arg.any()), Times.once()) - .revoke(); - } - else { - writeFileSpy - .verify(_ => _("/a/b/output/main.js", Arg.string(), Arg.any()), Times.once()) - .verify(_ => _("/a/b/output/main2.js", Arg.string(), Arg.any()), Times.once()) - .verify(_ => _(Arg.string(), Arg.string(), Arg.any()), Times.exactly(2)) - .revoke(); - } + 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); + }; + createWatchOfConfigFile(configFile.path, host); + if (useOutFile) { + // Only out file + assert.equal(mapOfFilesWritten.size, 1); } - - it("with --outFile and multiple declaration files in the program", () => { - verifyFilesEmittedOnce(/*useOutFile*/ true); + else { + // main.js and main2.js + assert.equal(mapOfFilesWritten.size, 2); + } + mapOfFilesWritten.forEach((value, key) => { + assert.equal(value, 1, "Key: " + key); }); + } - it("without --outFile and multiple declaration files in the program", () => { - verifyFilesEmittedOnce(/*useOutFile*/ false); - }); + it("with --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ true); }); - describe("emit", () => { - function writeFile(host: fakes.FakeServerHost, path: string, content: string) { - vfsutils.writeFile(host.vfs, path, content); + 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 + createWatchOfConfigFile(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 writeConfigFile(host: fakes.FakeServerHost, path: string, config: any = {}) { - const compilerOptions = (config.compilerOptions || (config.compilerOptions = {})); - compilerOptions.listEmittedFiles = true; - writeFile(host, path, JSON.stringify(config)); + function getFile(fileName: string) { + return find(files, file => file.path === fileName); } - function waitAndCheckAffectedFiles(host: fakes.FakeServerHost, affectedFiles: ReadonlyArray, unaffectedFiles?: ReadonlyArray) { + function verifyAffectedAllFiles() { + host.reloadFS(files); host.checkTimeoutQueueLengthAndRun(1); - checkAffectedFiles(host, affectedFiles, unaffectedFiles); - } - - function checkAffectedFiles(host: fakes.FakeServerHost, affectedFiles: ReadonlyArray, unaffectedFiles?: ReadonlyArray) { - affectedFiles = getEmittedLines(affectedFiles, host, formatOutputFile); - host.checkOutputContains(affectedFiles); - if (unaffectedFiles) { - unaffectedFiles = getEmittedLines(unaffectedFiles, host, formatOutputFile); - unaffectedFiles = mapDefined(unaffectedFiles, line => contains(affectedFiles, line) ? undefined : line); - host.checkOutputDoesNotContain(unaffectedFiles); - } + checkOutputContains(host, allEmittedFiles); 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: fakes.FakeServerHost) { - writeFile(host, file1Path, `let x = 1`); - writeFile(host, file2Path, `let y = 1`); - } - - it("if neither is set", () => { - const host = new fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(configFilePath, host); - checkAffectedFiles(host, commonOutputPaths); - - writeFile(host, file1Path, `let x = 11`); - waitAndCheckAffectedFiles(host, [file1OutputPath], commonOutputPaths); - }); - - it("if --out is set", () => { - const host = new fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath, { compilerOptions: { out: "/a/out.js" } }); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath, { compilerOptions: { outFile: "/a/out.js" } }); - - createWatchOfConfigFile(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: fakes.FakeServerHost, 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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(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.unlinkSync(file1Consumer2Path); - waitAndCheckAffectedFiles(host, [moduleFile1OutputPath, file1Consumer1OutputPath], commonOutputPaths); - }); - - it("should be up-to-date with newly created files", () => { - const host = new fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host, [file1Consumer1Path, moduleFile1Path]); - writeConfigFile(host, configFilePath, { files: [file1Consumer1Path] }); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath, { compilerOptions: { isolatedModules: true } }); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath, { compilerOptions: { module: "system", outFile: "/a/b/out.js" } }); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeCommonFiles(host); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ 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); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ lib: true }); - writeFile(host, "/a/b/referenceFile1.ts", `/// \nexport var x = Foo();`); - writeCommonFiles(host, [moduleFile1Path]); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(configFilePath, host); - checkAffectedFiles(host, ["/a/b/referenceFile1.js", moduleFile1OutputPath]); - - host.vfs.unlinkSync(moduleFile1Path); - waitAndCheckAffectedFiles(host, ["/a/b/referenceFile1.js"], [moduleFile1OutputPath]); - }); - - it("should detect non-existing code file", () => { - const host = new fakes.FakeServerHost({ lib: true }); - writeFile(host, "/a/b/referenceFile1.ts", `/// \nexport var x = Foo();`); - writeConfigFile(host, configFilePath); - - createWatchOfConfigFile(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 fakes.FakeServerHost({ newLine }); - writeFile(host, "/a/app.ts", `var x = 1;${newLine}var y = 2;`); - - createWatchOfFilesAndCompilerOptions(["/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 host = new fakes.FakeServerHost({ 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"); - - const writeFileSpy1 = spy(host, "writeFile"); - - createWatchOfConfigFile("/a/b/tsconfig.json", host); - checkAffectedFiles(host, ["/a/b/f1.js", "/a/b/f2.js", "/a/b/f3.js"]); - - writeFileSpy1 - .verify(_ => _("/a/b/f1.js", `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`, Arg.any()), Times.once()) - .verify(_ => _("/a/b/f2.js", `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, Arg.any()), Times.once()) - .verify(_ => _("/a/b/f3.js", `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n`, Arg.any()), Times.once()) - .revoke(); - - const writeFileSpy2 = spy(host, "writeFile"); - - 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"]); - - writeFileSpy2 - .verify(_ => _("/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`, Arg.any()), Times.once()) - .verify(_ => _("/a/b/f2.js", `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, Arg.any()), Times.once()) - .verify(_ => _("/a/b/f3.js", Arg.string(), Arg.any()), Times.none()) - .revoke(); - }); - - it("Elides const enums correctly in incremental compilation", () => { - const host = new fakes.FakeServerHost({ 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;`); - - const writeFileSpy1 = spy(host, "writeFile"); - - createWatchOfFilesAndCompilerOptions(["/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"]); - - writeFileSpy1 - .verify(_ => _("/user/someone/projects/myproject/file1.js", `"use strict";\nexports.__esModule = true;\n`, Arg.any()), Times.once()) - .verify(_ => _("/user/someone/projects/myproject/file2.js", `"use strict";\nexports.__esModule = true;\n`, Arg.any()), Times.once()) - .verify(_ => _("/user/someone/projects/myproject/file3.js", `"use strict";\nexports.__esModule = true;\nvar v = 1 /* V */;\n`, Arg.any()), Times.once()) - .revoke(); - - const writeFileSpy2 = spy(host, "writeFile"); - - 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"]); - - writeFileSpy2 - .verify(_ => _("/user/someone/projects/myproject/file1.js", `"use strict";\nexports.__esModule = true;\nfunction foo2() { return 2; }\n`, Arg.any()), Times.once()) - .verify(_ => _("/user/someone/projects/myproject/file2.js", Arg.string(), Arg.any()), Times.none()) - .verify(_ => _("/user/someone/projects/myproject/file3.js", Arg.string(), Arg.any()), Times.none()) - .revoke(); - }); - }); - }); - - describe("module resolution caching", () => { - it("works", () => { - const rootContent1 = `import {x} from "f1"`; - const importedContent = `foo()`; - - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/d/f0.ts": rootContent1, - "/a/f1.ts": importedContent, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/d/f0.ts"], host, { module: ModuleKind.AMD }); - - // 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") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - // spy on calls to fileExists to make sure that disk is not touched - const fileExistsSpy1 = spy(host, "fileExists"); - - // write file and trigger synchronization - const rootContent2 = `import {x} from "f1"\nvar x: string = 1;`; - host.vfs.writeFileSync("/a/d/f0.ts", rootContent2); - host.runQueuedTimeoutCallbacks(); - - // verify fileExists was not called. - fileExistsSpy1 - .verify(_ => _(Arg.any()), Times.none()) - .revoke(); - - // ensure file has correct number of errors after edit - checkOutputErrors(host, [ - 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") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - // spy on calls to fileExists to make sure LSHost only searches for 'f2' - const fileExistsSpy2 = spy(host, "fileExists"); - - // write file and trigger synchronization - const rootContent3 = `import {x} from "f2"`; - host.vfs.writeFileSync("/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()) - .revoke(); - - // ensure file has correct number of errors after edit - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/d/f0.ts", rootContent3, "f2") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - // spy on calls to fileExists to make sure LSHost only searches for 'f1' - const fileExistsSpy3 = spy(host, "fileExists"); - - // write file and trigger synchronization - const rootContent4 = `import {x} from "f1"`; - host.vfs.writeFileSync("/a/d/f0.ts", rootContent4); - host.runQueuedTimeoutCallbacks(); - - // verify fileExists was called correctly - fileExistsSpy3 - .verify(_ => _(Arg.includes("/f1.")), Times.atLeastOnce()) - .verify(_ => _(Arg.not(Arg.includes("/f1."))), Times.none()) - .revoke(); - - checkOutputErrors(host, [ - createFileIsNotAModuleDiagnostic(watch(), "/a/d/f0.ts", rootContent1, "f1", "/a/f1.ts"), - createCannotFindNameDiagnostic(watch(), "/a/f1.ts", importedContent, "foo") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - it("loads missing files from disk", () => { - const rootContent1 = `import {x} from "bar"`; - - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/foo.ts": rootContent1, - }); - - // spy on calls to fileExists when starting watch mode - const fileExistsSpy1 = spy(host, "fileExists"); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/foo.ts"], host, { module: ModuleKind.AMD }); - - // verify fileExists was called correctly - fileExistsSpy1 - .verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()) - .revoke(); - - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/foo.ts", rootContent1, "bar") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - // spy on calls to fileExists after synchronization is triggered - const fileExistsSpy2 = spy(host, "fileExists"); - - host.vfs.writeFileSync("/a/foo.ts", `import {y} from "bar"`); - host.vfs.writeFileSync("/a/bar.d.ts", `export const y = 1;`); - host.runQueuedTimeoutCallbacks(); - - // verify fileExists was called correctly - fileExistsSpy2 - .verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()) - .revoke(); - - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - 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;`; - - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/foo.ts": rootContent, - "/a/bar.d.ts": importedContent, - }); - - // spy on fileExists when starting watch mode - const fileExistsSpy1 = spy(host, "fileExists"); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/foo.ts"], host, { module: ModuleKind.AMD }); - - // verify fileExists was called correctly - fileExistsSpy1 - .verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()) - .revoke(); - - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - // spy on fileExists when triggering synchronization - const fileExistsSpy2 = spy(host, "fileExists"); - - host.vfs.unlinkSync("/a/bar.d.ts"); - host.runQueuedTimeoutCallbacks(); - - // verify fileExists was called correctly - fileExistsSpy2 - .verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()) - .revoke(); - - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/foo.ts", rootContent, "bar") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - // spy on fileExists when triggering synchronization - const fileExistsSpy3 = spy(host, "fileExists"); - - host.vfs.writeFileSync("/a/bar.d.ts", importedContent); + 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(); + } + } - // verify fileExists was called correctly. - fileExistsSpy3 - .verify(_ => _(Arg.includes("/bar.")), Times.atLeastOnce()) - .revoke(); + 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(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); + // 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]); - it("works when module resolution changes to ambient module", () => { - const rootContent = `import * as fs from "fs";`; - const host = new fakes.FakeServerHost({ vfs: { currentDirectory: "/a/b" }, lib: true }, /*files*/ { - "/a/b/foo.ts": rootContent, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/foo.ts"], host, { }); - - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/b/foo.ts", rootContent, "fs") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - host.writeFile("/a/b/node_modules/@types/node/package.json", `{\n "main": ""\n}\n`); - host.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, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - 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 fakes.FakeServerHost({ vfs: { currentDirectory: "/a/b" }, lib: true }, /*files*/ { - "/a/b/foo.ts": rootContent, - "/a/b/bar.d.ts": fileContent1, - }); - - const watch = createWatchOfFilesAndCompilerOptions(["/a/b/foo.ts", "/a/b/bar.d.ts"], host, {}); - - checkOutputErrors(host, [ - createCannotFindModuleDiagnostic(watch(), "/a/b/foo.ts", rootContent, "fs") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - const fileContent2 = fileContent1 + `declare module "fs" {\n export interface Stats {\n isFile(): boolean;\n }\n}`; - host.vfs.writeFileSync("/a/b/bar.d.ts", fileContent2); - host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - }); - - 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 fakes.FakeServerHost({ vfs: { currentDirectory: "/a/b/projects/myProject/" }, lib: true }, /*files*/ { - "/a/b/projects/myProject/src/file1.ts": file1Content, - "/a/b/projects/myProject/src/file2.ts": file2Content, - "/a/b/projects/myProject/node_modules/module1/index.js": `module.exports = options => { return options.toString(); }`, - "/a/b/projects/myProject/src/tsconfig.json": JSON.stringify({ - compilerOptions: { - allowJs: true, - rootDir: ".", - outDir: "../dist", - moduleResolution: "node", - maxNodeModuleJsDepth: 1 - } - }) - }); - - // spy on calls to writeFile when starting watch mode - const writeFileSpy1 = spy(host, "writeFile"); - - const watch = createWatchOfConfigFile("/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", - fakes.FakeServerHost.libPath - ]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); - - // 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(_ => _(Arg.not(Arg.or("/a/b/projects/myProject/dist/file1.js", "/a/b/projects/myProject/dist/file2.js")), Arg.string(), Arg.any()), Times.none()) - .revoke(); - - // spy on calls to writeFile when triggering synchronization - const writeFileSpy2 = spy(host, "writeFile"); - - host.vfs.writeFileSync("/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", - fakes.FakeServerHost.libPath - ]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); - - // verify writeFile was called correctly - writeFileSpy2 - .verify(_ => _("/a/b/projects/myProject/dist/file1.js", file1Output + ";\n", Arg.any()), Times.once()) - .verify(_ => _(Arg.not("/a/b/projects/myProject/dist/file1.js"), Arg.string(), Arg.any()), Times.none()) - .revoke(); - }); + // 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]); }); - describe("with when module emit is specified as node", () => { - it("when instead of filechanged recursive directory watcher is invoked", () => { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/rootFolder/project/Scripts/TypeScript.ts": `var z = 10;`, - "/a/rootFolder/project/Scripts/Javascript.js": `var zz = 10;`, - "/a/rootFolder/project/tsconfig.json": JSON.stringify({ - compilerOptions: { - module: "none", - allowJs: true, - outDir: "Static/scripts/" - }, - include: [ - "Scripts/**/*" - ], - }) - }); + it("should be up-to-date with the reference map changes", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + verifyAffectedFiles + } = getInitialState(); - const watch = createWatchOfConfigFile("/a/rootFolder/project/tsconfig.json", host); + // Change file1Consumer1 content to `export let y = Foo();` + file1Consumer1.content = `export let y = Foo();`; + verifyAffectedFiles([file1Consumer1]); - checkProgramActualFiles(watch(), [ - "/a/rootFolder/project/Scripts/TypeScript.ts", - "/a/rootFolder/project/Scripts/Javascript.js", - fakes.FakeServerHost.libPath - ]); + // 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]); - host.watchFiles = false; - host.vfs.unlinkSync("/a/rootFolder/project/Scripts/TypeScript.ts"); - host.runQueuedTimeoutCallbacks(); + // 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]); - const writeFileSpy1 = spy(host, "writeFile"); + // Multiple file edits in one go: - host.vfs.writeFileSync("/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", - fakes.FakeServerHost.libPath - ]); - - writeFileSpy1 - .verify(_ => _("/a/rootFolder/project/Static/scripts/TypeScript.js", `var zz30 = 100;\n`, Arg.any()), Times.once()) - .revoke(); - }); + // 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]); }); - describe("console clearing", () => { - function checkConsoleClearing(diagnostics: boolean, extendedDiagnostics: boolean) { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/f.ts": "" + 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) }); - let clearCount: number | undefined; - checkConsoleClears(); - createWatchOfFilesAndCompilerOptions(["/a/f.ts"], host, { diagnostics, extendedDiagnostics }); - checkConsoleClears(); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1]); + }); - host.writeFile("/a/f.ts", "//"); - host.runQueuedTimeoutCallbacks(); + 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] + }); - checkConsoleClears(); + const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path); + file1Consumer1.content += "export var T: number;"; + verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]); - function checkConsoleClears() { - if (clearCount === undefined) { - clearCount = 0; - } - else if (!diagnostics && !extendedDiagnostics) { - clearCount++; - } - host.checkScreenClears(clearCount); - return clearCount; + // 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 + }; + } + ); + } + 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`); + } + else { + assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`); } } + } - it("without --diagnostics or --extendedDiagnostics", () => { - checkConsoleClearing(/*diagnostics*/ false, /*extendedDiagnostics*/ false); - }); - it("with --diagnostics", () => { - checkConsoleClearing(/*diagnostics*/ true, /*extendedDiagnostics*/ false); - }); - it("with --extendedDiagnostics", () => { - checkConsoleClearing(/*diagnostics*/ false, /*extendedDiagnostics*/ true); - }); + 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); + + // Initial compile + if (configFile) { + createWatchOfConfigFile(configFile.path, host); + } + else { + // First file as the root + createWatchOfFilesAndCompilerOptions([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("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); + } + }); + + 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]]; + } + }); + }); + + 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()` + }; + + const files = [root, imported, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + 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"); + + // ensure that imported file was found + checkOutputErrors(host, [f1IsNotModule, cannotFindFoo], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + const originalFileExists = host.fileExists; + { + const newContent = `import {x} from "f1" + var x: string = 1;`; + root.content = newContent; + host.reloadFS(files); + + // 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 + host.runQueuedTimeoutCallbacks(); + + // 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 + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + } + { + 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); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrors(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + + 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); + }; + + const newContent = `import {x} from "f1"`; + root.content = newContent; + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + checkOutputErrors(host, [f1IsNotModule, cannotFindFoo], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + assert.isTrue(fileExistsCalled); + } + }); + + it("loads missing files from disk", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + + const files = [root, libFile]; + const host = createWatchedSystem(files); + 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); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrors(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + fileExistsCalledForBar = false; + root.content = `import {y} from "bar"`; + host.reloadFS(files.concat(imported)); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + 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"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;export const x = 10;` + }; + + 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); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + fileExistsCalledForBar = false; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputErrors(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + + fileExistsCalledForBar = false; + host.reloadFS(filesWithImported); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("works when module resolution changes to ambient module", () => { + const root = { + path: "/a/b/foo.ts", + content: `import * as fs from "fs";` + }; + + const packageJson = { + path: "/a/b/node_modules/@types/node/package.json", + content: ` +{ + "main": "" +} +` + }; + + const nodeType = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +}` + }; + + const files = [root, libFile]; + const filesWithNodeType = files.concat(packageJson, nodeType); + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); + + checkOutputErrors(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + host.reloadFS(filesWithNodeType); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + + 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"; +` + }; + + const file = { + path: "/a/b/bar.d.ts", + content: ` +declare module "url" { + export interface Url { + href?: string; + } +} +` + }; + + const fileContentWithFS = ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +} +`; + + const files = [root, file, libFile]; + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); + + checkOutputErrors(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + + file.content += fileContentWithFS; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + }); + + 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({ + compilerOptions: { + allowJs: true, + rootDir: ".", + outDir: "../dist", + 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 = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + 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, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + verifyExpectedFiles(expectedFiles); + + + 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); + } + }); + } + + function createExpectedToNotEmitFile(fileName: string): ExpectedFile { + return { + path: outDirFolder + fileName, + isExpectedToEmit: false + }; + } + + 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" + }; + } + }); + }); + + 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({ + compilerOptions: { + module: "none", + allowJs: true, + outDir: "Static/scripts/" + }, + 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 = createWatchOfConfigFile(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(); + + 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); + }); + }); + + describe("tsc-watch console clearing", () => { + function checkConsoleClearing(diagnostics: boolean, extendedDiagnostics: boolean) { + const file = { + path: "f.ts", + content: "" + }; + const files = [file]; + const host = createWatchedSystem(files); + let clearCount: number | undefined; + checkConsoleClears(); + + createWatchOfFilesAndCompilerOptions([file.path], host, { diagnostics, extendedDiagnostics }); + checkConsoleClears(); + + file.content = "//"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + checkConsoleClears(); + + function checkConsoleClears() { + if (clearCount === undefined) { + clearCount = 0; + } + else if (!diagnostics && !extendedDiagnostics) { + clearCount++; + } + host.checkScreenClears(clearCount); + return clearCount; + } + } + + it("without --diagnostics or --extendedDiagnostics", () => { + checkConsoleClearing(/*diagnostics*/ false, /*extendedDiagnostics*/ false); + }); + it("with --diagnostics", () => { + checkConsoleClearing(/*diagnostics*/ true, /*extendedDiagnostics*/ false); + }); + it("with --extendedDiagnostics", () => { + checkConsoleClearing(/*diagnostics*/ false, /*extendedDiagnostics*/ true); }); }); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 60df22d95f7..42199c895b1 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1,24 +1,20 @@ -/// +/// +/// /// -/// -/// -/// namespace ts.projectSystem { - import spy = typemock.spy; - import dedent = utils.dedent; import TI = server.typingsInstaller; import protocol = server.protocol; import CommandNames = server.CommandNames; - interface FileOrFolder { - path: string; - content?: string; - } - - import checkFileNames = utils.checkFileNames; - - const libFile = { path: fakes.FakeServerHost.libPath, content: fakes.FakeServerHost.libContent }; + export import TestServerHost = ts.TestFSWithWatch.TestServerHost; + export type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + export import createServerHost = ts.TestFSWithWatch.createServerHost; + export import checkFileNames = ts.TestFSWithWatch.checkFileNames; + export import libFile = ts.TestFSWithWatch.libFile; + export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; + import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; + import safeList = ts.TestFSWithWatch.safeList; export const customTypesMap = { path: "/typesMap.json", @@ -69,7 +65,7 @@ namespace ts.projectSystem { installTypingHost: server.ServerHost, readonly typesRegistry = createMap(), log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, fakes.FakeServerHost.safeListPath, customTypesMap.path, throttleLimit, log); + super(installTypingHost, globalTypingsCacheLocation, safeList.path, customTypesMap.path, throttleLimit, log); } protected postExecActions: PostExecAction[] = []; @@ -103,7 +99,7 @@ namespace ts.projectSystem { this.addPostExecAction("success", cb); } - sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.BeginInstallTypes | server.EndInstallTypes) { + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { this.projectService.updateTypingsForProject(response); } @@ -146,9 +142,9 @@ namespace ts.projectSystem { private events: server.ProjectServiceEvent[] = []; readonly session: TestSession; readonly service: server.ProjectService; - readonly host: fakes.FakeServerHost; - constructor(host: fakes.FakeServerHost) { - this.host = host; + readonly host: projectSystem.TestServerHost; + constructor(files: projectSystem.FileOrFolder[]) { + this.host = createServerHost(files); this.session = createSession(this.host, { canUseEvents: true, eventHandler: event => this.events.push(event), @@ -190,7 +186,7 @@ namespace ts.projectSystem { assertProjectInfoTelemetryEvent(partial: Partial, configFile?: string): void { assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), { - projectId: core.sha1(configFile || "/tsconfig.json"), + projectId: Harness.mockHash(configFile || "/tsconfig.json"), fileStats: fileStats({ ts: 1 }), compilerOptions: {}, extends: false, @@ -215,7 +211,7 @@ namespace ts.projectSystem { class TestSession extends server.Session { private seq = 0; public events: protocol.Event[] = []; - public host: fakes.FakeServerHost; + public host: TestServerHost; getProjectService() { return this.projectService; @@ -299,7 +295,6 @@ namespace ts.projectSystem { checkNumberOfProjects(this, count); } } - export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; const logger = parameters.logger || nullLogger; @@ -418,9 +413,9 @@ namespace ts.projectSystem { } } - export function makeSessionRequest(command: string, args: T, seq = 0) { + export function makeSessionRequest(command: string, args: T) { const newRequest: protocol.Request = { - seq, + seq: 0, type: "request", command, arguments: args @@ -428,29 +423,9 @@ namespace ts.projectSystem { return newRequest; } - 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[], session: server.Session) { + export function openFilesForSession(files: FileOrFolder[], session: server.Session) { for (const file of files) { - const request = makeSessionRequest(CommandNames.Open, { file }); + const request = makeSessionRequest(CommandNames.Open, { file: file.path }); session.executeCommand(request); } } @@ -498,6434 +473,6370 @@ namespace ts.projectSystem { } } - function createServerHost(files: FileOrFolder[], options?: Partial) { - return new fakes.FakeServerHost(options, files.reduce((map, file) => (map[file.path] = isString(file.content) ? file.content : {}, map), {} as vfs.FileSet)); - } - - function getDiffInKeys(map: Map, expectedKeys: ReadonlyArray) { - if (map.size === expectedKeys.length) { - return ""; - } - const notInActual: string[] = []; - const duplicates: string[] = []; - const seen = createMap(); - forEach(expectedKeys, expectedKey => { - if (seen.has(expectedKey)) { - duplicates.push(expectedKey); - return; - } - seen.set(expectedKey, true); - if (!map.has(expectedKey)) { - notInActual.push(expectedKey); - } - }); - const inActualNotExpected: string[] = []; - map.forEach((_value, key) => { - if (!seen.has(key)) { - inActualNotExpected.push(key); - } - seen.set(key, true); - }); - return `\n\nNotInActual: ${notInActual}\nDuplicates: ${duplicates}\nInActualButNotInExpected: ${inActualNotExpected}`; - } - - function verifyMapSize(caption: string, map: Map, expectedKeys: ReadonlyArray) { - assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`); - } - - describe("tsserverProjectSystem", () => { - describe("general functionality", () => { - describe("general", () => { - const commonFile1: FileOrFolder = { - path: "/a/b/commonFile1.ts", - content: "let x = 1" - }; - const commonFile2: FileOrFolder = { - path: "/a/b/commonFile2.ts", - content: "let y = 1" - }; - - it("create inferred project", () => { - const host = new fakes.FakeServerHost({ lib: true }, /*files*/ { - "/a/b/c/app.ts": `import {f} from "./module"\nconsole.log(f)`, - "/a/b/c/module.d.ts": `export let x: number`, - }); - - const projectService = createProjectService(host); - const { configFileName } = projectService.openClientFile("/a/b/c/app.ts"); - - assert(!configFileName, `should not find config, got: '${configFileName}`); - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 1); - - const project = projectService.inferredProjects[0]; - - checkFileNames("inferred project", project.getFileNames(), ["/a/b/c/app.ts", fakes.FakeServerHost.libPath, "/a/b/c/module.d.ts"]); - const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; - const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - host.checkWatchedFiles(configFiles.concat(fakes.FakeServerHost.libPath, "/a/b/c/module.d.ts")); - host.checkWatchedDirectories([], /*recursive*/ false); - host.checkWatchedDirectories(["/a/b/c", combinePaths("/a/b/c/", nodeModulesAtTypes)], /*recursive*/ true); - }); - - it("can handle tsconfig file name with difference casing", () => { - const host = new fakes.FakeServerHost({ vfs: { useCaseSensitiveFileNames: false } }, /*files*/ { - "/a/b/app.ts": `let x = 1`, - "/a/b/tsconfig.json": `{ "include": [] }`, - }); - - const service = createProjectService(host); - service.openExternalProject({ - projectFileName: "/a/b/project.csproj", - rootFiles: toExternalFiles(["/a/b/app.ts", "/A/B/tsconfig.json"]), - options: {} - }); - service.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(service, 0), ["/A/B/tsconfig.json"]); - - service.openClientFile("/a/b/app.ts"); - service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - - checkProjectActualFiles(configuredProjectAt(service, 0), ["/A/B/tsconfig.json"]); - checkProjectActualFiles(service.inferredProjects[0], ["/a/b/app.ts"]); - }); - - 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 = createServerHost([configFile, file1, file2, file3], { lib: true }); - const projectService = createProjectService(host); - const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); - - assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - checkNumberOfInferredProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, file2.path, configFile.path]); - checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - host.checkWatchedFiles([configFile.path, file2.path, fakes.FakeServerHost.libPath]); - const configFileDirectory = getDirectoryPath(configFile.path); - host.checkWatchedDirectories([configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); - }); - - it("create configured project with the file list", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: ` - { - "compilerOptions": {}, - "include": ["*.ts"] - }` - }; - const file1: FileOrFolder = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2: FileOrFolder = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const file3: FileOrFolder = { - path: "/a/b/c/f3.ts", - content: "let z = 1" - }; - - const host = createServerHost([configFile, libFile, file1, file2, file3]); - const projectService = createProjectService(host); - const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); - - assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - checkNumberOfInferredProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, file2.path, configFile.path]); - checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - host.checkWatchedFiles([configFile.path, file2.path, fakes.FakeServerHost.libPath]); - host.checkWatchedDirectories([getDirectoryPath(configFile.path)], /*recursive*/ false); - }); - - it("add and then remove a config file in a folder with loose files", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "files": ["commonFile1.ts"] - }` - }; - const filesWithoutConfig = [libFile, commonFile1, commonFile2]; - const host = createServerHost(filesWithoutConfig); - - // const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - projectService.openClientFile(commonFile2.path); - - checkNumberOfInferredProjects(projectService, 2); - const configFileLocations = ["/", "/a/", "/a/b/"]; - const watchedFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]).concat(fakes.FakeServerHost.libPath); - host.checkWatchedFiles(watchedFiles); - - // Add a tsconfig file - host.writeFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfInferredProjects(projectService, 1); - checkNumberOfConfiguredProjects(projectService, 1); - host.checkWatchedFiles(watchedFiles); - - // remove the tsconfig file - host.vfs.unlinkSync(configFile.path); - - checkNumberOfInferredProjects(projectService, 1); - host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects - - checkNumberOfInferredProjects(projectService, 2); - checkNumberOfConfiguredProjects(projectService, 0); - host.checkWatchedFiles(watchedFiles); - }); - - it("add new files to a configured project without file list", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([commonFile1, libFile, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - const configFileDir = getDirectoryPath(configFile.path); - host.checkWatchedDirectories([configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path]); - - // add a new ts file - host.writeFile(commonFile2.path, commonFile2.content); - host.checkTimeoutQueueLengthAndRun(2); - // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. - checkProjectRootFiles(project, [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 = createServerHost([commonFile1, commonFile2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - projectService.openClientFile(commonFile2.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path]); - checkNumberOfInferredProjects(projectService, 1); - }); - - it("remove not-listed external projects", () => { - const f1 = { - path: "/a/app.ts", - content: "let x = 1" - }; - const f2 = { - path: "/b/app.ts", - content: "let x = 1" - }; - const f3 = { - path: "/c/app.ts", - content: "let x = 1" - }; - const makeProject = (f: FileOrFolder) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); - const p1 = makeProject(f1); - const p2 = makeProject(f2); - const p3 = makeProject(f3); - - const host = createServerHost([f1, f2, f3]); - const session = createSession(host); - - session.executeCommand({ - seq: 1, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1, p2] } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { externalProjects: 2 }); - assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); - assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); - - session.executeCommand({ - seq: 2, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1, p3] } - }); - checkNumberOfProjects(projectService, { externalProjects: 2 }); - assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); - assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); - - session.executeCommand({ - seq: 3, - type: "request", - command: "openExternalProjects", - arguments: { projects: [] } - }); - checkNumberOfProjects(projectService, { externalProjects: 0 }); - - session.executeCommand({ - seq: 3, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p2] } - }); - assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); - }); - - it("handle recreated files correctly", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([commonFile1, commonFile2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - - // delete commonFile2 - host.vfs.unlinkSync(commonFile2.path); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(project, [commonFile1.path]); - - // re-add commonFile2 - host.writeFile(commonFile2.path, commonFile2.content); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - }); - - it("handles the missing files - that were added to program because they were added with /// { - const file1: FileOrFolder = { - path: "/a/b/commonFile1.ts", - content: `/// - let x = y` - }; - const host = createServerHost([file1, libFile]); - const session = createSession(host); - openFilesForSession([file1.path], session); - const projectService = session.getProjectService(); - - checkNumberOfInferredProjects(projectService, 1); - const project = projectService.inferredProjects[0]; - checkProjectRootFiles(project, [file1.path]); - checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath]); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - - // Two errors: CommonFile2 not found and cannot find name y - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, - { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } - ]); - - host.writeFile(commonFile2.path, commonFile2.content); - host.runQueuedTimeoutCallbacks(); - checkNumberOfInferredProjects(projectService, 1); - assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); - checkProjectRootFiles(project, [file1.path]); - checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, commonFile2.path]); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("should create new inferred projects for files excluded from a configured project", () => { - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}", "${commonFile2.path}"] - }` - }; - const files = [commonFile1, commonFile2, configFile]; - const host = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - configFile.content = `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}"] - }`; - host.writeFile(configFile.path, configFile.content); - - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(project, [commonFile1.path]); - - projectService.openClientFile(commonFile2.path); - checkNumberOfInferredProjects(projectService, 1); - }); - - 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 = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(commonFile1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - projectService.openClientFile(excludedFile1.path); - checkNumberOfInferredProjects(projectService, 1); - }); - - 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 = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.openClientFile(nodeModuleFile.path); - projectService.openClientFile(classicModuleFile.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); - checkNumberOfInferredProjects(projectService, 1); - - configFile.content = `{ - "compilerOptions": { - "moduleResolution": "classic" - }, - "files": ["${file1.path}"] - }`; - host.writeFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); - checkNumberOfInferredProjects(projectService, 1); - }); - - it("should keep the configured project when the opened file is referenced by the project but not its root", () => { - const file1: FileOrFolder = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: FileOrFolder = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - }); - - it("should keep the configured project when the opened file is referenced by the project but not its root", () => { - const file1: FileOrFolder = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: FileOrFolder = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - }); - - 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 = createServerHost([commonFile1, commonFile2, libFile, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); - }); - - it("should disable features when the files are too large", () => { - const file1 = { - path: "/a/b/f1.js", - content: "let x =1;", - fileSize: 10 * 1024 * 1024 - }; - const file2 = { - path: "/a/b/f2.js", - content: "let y =1;", - fileSize: 6 * 1024 * 1024 - }; - const file3 = { - path: "/a/b/f3.js", - content: "let y =1;", - fileSize: 6 * 1024 * 1024 - }; - - const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3"; - - const host = createServerHost([file1, file2, file3]); - const getFileSizeSpy = spy(host, "getFileSize") - .setup(_ => _(file1.path), { return: file1.fileSize }) - .setup(_ => _(file2.path), { return: file2.fileSize }) - .setup(_ => _(file3.path), { return: file3.fileSize }); - - const projectService = createProjectService(host); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); - const proj1 = projectService.findProject(proj1name); - assert.isTrue(proj1.languageServiceEnabled); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); - const proj2 = projectService.findProject(proj2name); - assert.isTrue(proj2.languageServiceEnabled); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); - const proj3 = projectService.findProject(proj3name); - assert.isFalse(proj3.languageServiceEnabled); - - getFileSizeSpy.revoke(); - }); - - it("should use only one inferred project if 'useOneInferredProject' is set", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const file2 = { - path: "/a/c/main.ts", - content: "let x =1;" - }; - - const file3 = { - path: "/a/d/main.ts", - content: "let x =1;" - }; - - const host = createServerHost([file1, file2, file3, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - projectService.openClientFile(file3.path); - - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, fakes.FakeServerHost.libPath]); - - - host.writeFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, fakes.FakeServerHost.libPath]); - }); - - it("should reuse same project if file is opened from the configured project that has no open files", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/b/main2.ts", - content: "let y =1;" - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts", "main2.ts" ] - }` - }; - const host = createServerHost([file1, file2, configFile, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path); - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isFalse(project.hasOpenRef()); // No open files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isTrue(project.hasOpenRef()); // file2 - assert.isFalse(project.isClosed()); - }); - - it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6" - }, - "files": [ "main.ts" ] - }` - }; - const host = createServerHost([file1, configFile, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path); - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isFalse(project.hasOpenRef()); // No files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(fakes.FakeServerHost.libPath); - checkNumberOfConfiguredProjects(projectService, 0); - assert.isFalse(project.hasOpenRef()); // No files + project closed - assert.isTrue(project.isClosed()); - }); - - it("should not close external project with no open files", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y =1;" - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: toExternalFiles([file1.path, file2.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - // open client file - should not lead to creation of inferred project - projectService.openClientFile(file1.path, file1.content); - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - // close client file - external project should still exists - projectService.closeClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - projectService.closeExternalProject(externalProjectName); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 0); - }); - - it("external project for dynamic file", () => { - const externalProjectName = "^ScriptDocument1 file1.ts"; - const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]); - const host = createServerHost([]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: externalFiles, - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - externalFiles[0].content = "let x =1;"; - projectService.applyChangesInOpenFiles(externalFiles, [], []); - }); - - it("external project that included config files", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f1.ts"] - } - ) - }; - const file2 = { - path: "/a/c/f2.ts", - content: "let y =1;" - }; - const config2 = { - path: "/a/c/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f2.ts"] - } - ) - }; - const file3 = { - path: "/a/d/f3.ts", - content: "let z =1;" - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2, file3, config1, config2]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 2 }); - const proj1 = projectService.configuredProjects.get(config1.path); - const proj2 = projectService.configuredProjects.get(config2.path); - assert.isDefined(proj1); - assert.isDefined(proj2); - - // open client file - should not lead to creation of inferred project - projectService.openClientFile(file1.path, file1.content); - checkNumberOfProjects(projectService, { configuredProjects: 2 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); - - projectService.openClientFile(file3.path, file3.content); - checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); - - projectService.closeExternalProject(externalProjectName); - // open file 'file1' from configured project keeps project alive - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - - projectService.closeClientFile(file3.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - - projectService.openClientFile(file2.path, file2.content); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(config1.path)); - assert.isDefined(projectService.configuredProjects.get(config2.path)); - - }); - - it("reload regular file after closing", () => { - const f1 = { - path: "/a/b/app.ts", - content: "x." - }; - const f2 = { - path: "/a/b/lib.ts", - content: "let x: number;" - }; - - const host = createServerHost([f1, f2, libFile]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); - - service.openClientFile(f1.path); - service.openClientFile(f2.path, "let x: string"); - - service.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, fakes.FakeServerHost.libPath]); - - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - // should contain completions for string - assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); - assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); - - service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - // should contain completions for string - assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); - assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); - }); - - it("clear mixed content file after closing", () => { - const f1 = { - path: "/a/b/app.ts", - content: " " - }; - const f2 = { - path: "/a/b/lib.html", - content: "" - }; - - const host = createServerHost([f1, f2, libFile]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} }); - - service.openClientFile(f1.path); - service.openClientFile(f2.path, "let somelongname: string"); - - service.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, fakes.FakeServerHost.libPath]); - - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); - - service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); - const sf2 = service.externalProjects[0].getLanguageService().getProgram().getSourceFile(f2.path); - assert.equal(sf2.text, ""); - }); - - it("external project with included config file opened after configured project", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.openExternalProject({ - rootFiles: toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.closeClientFile(file1.path); - // configured project is alive since it is opened as part of external project - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.closeExternalProject(externalProjectName); - checkNumberOfProjects(projectService, { configuredProjects: 0 }); - }); - - it("external project with included config file opened after configured project and then closed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/f2.ts", - content: "let x = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2, libFile, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path); - - projectService.openExternalProject({ - rootFiles: toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.closeExternalProject(externalProjectName); - // configured project is alive since file is still open - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(configFile.path)); - }); - - it("changes in closed 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 = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); - - projectService.openClientFile(file3.path); - checkNumberOfInferredProjects(projectService, 2); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - const modifiedFile2 = { - path: file2.path, - content: `export * from "../c/f3"` // now inferred project should inclule file3 - }; - - host.writeFile(modifiedFile2.path, modifiedFile2.content); - host.checkTimeoutQueueLengthAndRun(2); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [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 = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - - host.vfs.unlinkSync(file2.path); - host.checkTimeoutQueueLengthAndRun(2); - - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - }); - - it("ignores files excluded by a custom safe type list", () => { - const file1 = { - path: "/a/b/f1.js", - content: "export let x = 5" - }; - const office = { - path: "/lib/duckquack-3.min.js", - content: "whoa do @@ not parse me ok thanks!!!" - }; - const host = createServerHost([file1, office, customTypesMap]); - const projectService = createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); - assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]); - } - finally { - projectService.resetSafeList(); - } - }); - - it("ignores files excluded by the default type list", () => { - const file1 = { - path: "/a/b/f1.js", - content: "export let x = 5" - }; - const minFile = { - path: "/c/moment.min.js", - content: "unspecified" - }; - const kendoFile1 = { - path: "/q/lib/kendo/kendo.all.min.js", - content: "unspecified" - }; - const kendoFile2 = { - path: "/q/lib/kendo/kendo.ui.min.js", - content: "unspecified" - }; - const kendoFile3 = { - path: "/q/lib/kendo-ui/kendo.all.js", - content: "unspecified" - }; - const officeFile1 = { - path: "/scripts/Office/1/excel-15.debug.js", - content: "unspecified" - }; - const officeFile2 = { - path: "/scripts/Office/1/powerpoint.js", - content: "unspecified" - }; - const files = [file1, minFile, kendoFile1, kendoFile2, kendoFile3, officeFile1, officeFile2]; - const host = createServerHost(files); - const projectService = createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); - assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]); - } - finally { - projectService.resetSafeList(); - } - }); - - it("removes version numbers correctly", () => { - const testData: [string, string][] = [ - ["jquery-max", "jquery-max"], - ["jquery.min", "jquery"], - ["jquery-min.4.2.3", "jquery"], - ["jquery.min.4.2.1", "jquery"], - ["minimum", "minimum"], - ["min", "min"], - ["min.3.2", "min"], - ["jquery", "jquery"] - ]; - for (const t of testData) { - assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]); - } - }); - - it("ignores files excluded by a legacy safe type list", () => { - const file1 = { - path: "/a/b/bliss.js", - content: "let x = 5" - }; - const file2 = { - path: "/a/b/foo.js", - content: "" - }; - const file3 = { - path: "/a/b/Bacon.js", - content: "let y = 5" - }; - const host = createServerHost([file1, file2, file3, customTypesMap]); - const projectService = createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(), [file2.path]); - } - finally { - projectService.resetSafeList(); - } - }); - - it("open file become a part of configured project if it is referenced from root 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 = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - host.writeFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.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 = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - }); - - 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 = createServerHost([file1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - host.writeFile(file2.path, file2.content); - - host.checkTimeoutQueueLengthAndRun(2); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [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 = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - host.writeFile(modifiedConfigFile.path, modifiedConfigFile.content); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [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 = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) - }; - - host.writeFile(modifiedConfigFile.path, modifiedConfigFile.content); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); - - it("can correctly update external project when set of root files has changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); - - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - }); - - it("can update external project when set of root files was not changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "m"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: "export let y = 1" - }; - const file3 = { - path: "/a/m.ts", - content: "export let y = 1" - }; - - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); - - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); - }); - - it("regression test for crash in acquireOrUpdateDocument", () => { - const tsFile = { - fileName: "/a/b/file1.ts", - path: "/a/b/file1.ts", - content: "" - }; - const jsFile = { - path: "/a/b/file1.js", - content: "var x = 10;", - fileName: "/a/b/file1.js", - scriptKind: "JS" as "JS" - }; - - const host = createServerHost([]); - const projectService = createProjectService(host); - projectService.applyChangesInOpenFiles([tsFile], [], []); - const projs = projectService.synchronizeProjectList([]); - projectService.findProject(projs[0].info.projectName).getLanguageService().getNavigationBarItems(tsFile.fileName); - projectService.synchronizeProjectList([projs[0].info]); - projectService.applyChangesInOpenFiles([jsFile], [], []); - }); - - 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 = createServerHost([file1, file2, config]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - host.vfs.unlinkSync(config.path); - host.checkTimeoutQueueLengthAndRun(1); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); - }); - - it("loading files with correct priority", () => { - const f1 = { - path: "/a/main.ts", - content: "let x = 1" - }; - const f2 = { - path: "/a/main.js", - content: "var y = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { allowJs: true } - }) - }; - const host = createServerHost([f1, f2, config]); - const projectService = createProjectService(host); - projectService.setHostConfiguration({ - extraFileExtensions: [ - { extension: ".js", isMixedContent: false }, - { extension: ".html", isMixedContent: true } - ] - }); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); - - // Should close configured project with next file open - projectService.closeClientFile(f1.path); - - projectService.openClientFile(f2.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(config.path)); - checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); - }); - - it("tsconfig script block support", () => { - const file1 = { - path: "/a/b/f1.ts", - content: ` ` - }; - const file2 = { - path: "/a/b/f2.html", - content: `var hello = "hello";` - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true } }) - }; - const host = createServerHost([file1, file2, config]); - const session = createSession(host); - openFilesForSession([file1.path], session); - const projectService = session.getProjectService(); - - // HTML file will not be included in any projects yet - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const configuredProj = configuredProjectAt(projectService, 0); - checkProjectActualFiles(configuredProj, [file1.path, config.path]); - - // Specify .html extension as mixed content - const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; - const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); - session.executeCommand(configureHostRequest); - - // The configured project should now be updated to include html file - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - // Open HTML file - projectService.applyChangesInOpenFiles( - /*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }], - /*changedFiles*/ undefined, - /*closedFiles*/ undefined); - - // Now HTML file is included in the project - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - // Check identifiers defined in HTML content are available in .ts file - const project = configuredProjectAt(projectService, 0); - let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); - - // Close HTML file - projectService.applyChangesInOpenFiles( - /*openFiles*/ undefined, - /*changedFiles*/ undefined, - /*closedFiles*/[file2.path]); - - // HTML file is still included in project - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); - - // Check identifiers defined in HTML content are not available in .ts file - completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); - }); - - it("no tsconfig script block diagnostic errors", () => { - - // #1. Ensure no diagnostic errors when allowJs is true - const file1 = { - path: "/a/b/f1.ts", - content: ` ` - }; - const file2 = { - path: "/a/b/f2.html", - content: `var hello = "hello";` - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true } }) - }; - - let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(fakes.FakeServerHost.libPath), "tsc.js") }); - let session = createSession(host); - - // Specify .html extension as mixed content in a configure host request - const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; - const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); - session.executeCommand(configureHostRequest); - - openFilesForSession([file1.path], session); - let projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #2. Ensure no errors when allowJs is false - const config2 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: false } }) - }; - - host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(fakes.FakeServerHost.libPath), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1.path], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #3. Ensure no errors when compiler options aren't specified - const config3 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({}) - }; - - host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(fakes.FakeServerHost.libPath), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1.path], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #4. Ensure no errors when files are explicitly specified in tsconfig - const config4 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] }) - }; - - host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(fakes.FakeServerHost.libPath), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1.path], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - - // #4. Ensure no errors when files are explicitly excluded in tsconfig - const config5 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] }) - }; - - host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(fakes.FakeServerHost.libPath), "tsc.js") }); - session = createSession(host); - - session.executeCommand(configureHostRequest); - - openFilesForSession([file1.path], session); - projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); - }); - - it("project structure update is deferred if files are not added\removed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `import {x} from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: "export let x = 1" - }; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - projectService.applyChangesInOpenFiles( - /*openFiles*/ undefined, - /*changedFiles*/[{ fileName: file1.path, changes: [{ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }] }], - /*closedFiles*/ undefined); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const changedFiles = projectService.getChangedFiles_TestOnly(); - assert(changedFiles && changedFiles.length === 1, `expected 1 changed file, got ${JSON.stringify(changedFiles && changedFiles.length || 0)}`); - - projectService.ensureInferredProjectsUpToDate_TestOnly(); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - }); - - it("files with mixed content are handled correctly", () => { - const file1 = { - path: "/a/b/f1.html", - content: `