diff --git a/src/harness/documentsUtil.ts b/src/harness/documentsUtil.ts index 1a05a3ac36e..b3663459bff 100644 --- a/src/harness/documentsUtil.ts +++ b/src/harness/documentsUtil.ts @@ -2,19 +2,13 @@ // support the eventual conversion of harness into a modular system. namespace documents { - export interface HarnessCompilerTestFile { - unitName: string; - content: string; - fileOptions?: any; - } - export class TextDocument { public readonly meta: Map; public readonly file: string; public readonly text: string; private _lineStarts: readonly number[] | undefined; - private _testFile: HarnessCompilerTestFile | undefined; + private _testFile: Harness.Compiler.TestFile | undefined; constructor(file: string, text: string, meta?: Map) { this.file = file; @@ -26,7 +20,7 @@ namespace documents { return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text)); } - public static fromTestFile(file: HarnessCompilerTestFile) { + public static fromTestFile(file: Harness.Compiler.TestFile) { return new TextDocument( file.unitName, file.content, diff --git a/src/harness/fakesHosts.ts b/src/harness/fakesHosts.ts index 201d11a81c1..988f79e2b35 100644 --- a/src/harness/fakesHosts.ts +++ b/src/harness/fakesHosts.ts @@ -1,3 +1,6 @@ +/** + * Fake implementations of various compiler dependencies. + */ namespace fakes { const processExitSentinel = new Error("System exit"); @@ -211,172 +214,7 @@ namespace fakes { } } - /** - * @implements {server.ServerHost} but that would create a circular dependency - */ - export class FakeServerHost extends System { - timeoutCallbacks = new Callbacks() - immediateCallbacks = new Callbacks() - readonly watchedFiles = ts.createMultiMap(); - readonly fsWatches = ts.createMultiMap(); - readonly fsWatchesRecursive = ts.createMultiMap(); - readonly currentDirectory: string // TODO: Needed? Probably not - readonly toPath: (f: string) => ts.Path; - watchFile: ts.HostWatchFile - watchDirectory: ts.HostWatchDirectory - constructor(vfs: vfs.FileSystem, options: SystemOptions = {}) { - super(vfs, options) - - this.currentDirectory = '/'; // THIS IS FINE - this.toPath = s => ts.toPath(s, this.currentDirectory, s => s); - - const { watchFile, watchDirectory } = ts.createSystemWatchFunctions({ - // We dont have polling watch file - // it is essentially fsWatch but lets get that separate from fsWatch and - // into watchedFiles for easier testing - pollingWatchFile: /*tscWatchFile === Tsc_WatchFile.SingleFileWatcherPerName ? - createSingleFileWatcherPerName( - this.watchFileWorker.bind(this), - this.useCaseSensitiveFileNames - ) :*/ - this.watchFileWorker.bind(this), - getModifiedTime: this.getModifiedTime.bind(this), - setTimeout: this.setTimeout.bind(this), - clearTimeout: this.clearTimeout.bind(this), - fsWatch: this.fsWatch.bind(this), - fileExists: this.fileExists.bind(this), - useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, - getCurrentDirectory: this.getCurrentDirectory.bind(this), - // TODO: Tests usually set "run without recursive watches" to true, except for two tests - // watchOptions/with excludeFiles option${...} - // and the same, but excludeDirectories - // .............guessing that it's true on a real FS - fsSupportsRecursiveFsWatch: true, /*tscWatchDirectory ? false : !runWithoutRecursiveWatches */ - directoryExists: this.directoryExists.bind(this), - getAccessibleSortedChildDirectories: path => this.getDirectories(path), - realpath: this.realpath.bind(this), - tscWatchFile: undefined, - tscWatchDirectory: undefined, - defaultWatchFileKind: () => undefined, // () => this.defaultWatchFileKind?.(), - }) - this.watchFile = watchFile - this.watchDirectory = watchDirectory - } - setTimeout(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { - return this.timeoutCallbacks.register(callback, args) - } - clearTimeout(timeoutId: any): void { - this.timeoutCallbacks.unregister(timeoutId) - } - setImmediate(callback: (...args: any[]) => void, ...args: any[]): any { - return this.immediateCallbacks.register(callback, args) - } - clearImmediate(timeoutId: any): void { - this.immediateCallbacks.unregister(timeoutId) - } - watchFileWorker(fileName: string, cb: ts.FileWatcherCallback, pollingInterval: ts.PollingInterval) { - console.log('watchfileworker') - return createWatcher( - this.watchedFiles, - this.toFullPath(fileName), - { fileName, cb, pollingInterval } - ); - } - - fsWatch( - fileOrDirectory: string, - _entryKind: ts.FileSystemEntryKind, - cb: ts.FsWatchCallback, - recursive: boolean, - fallbackPollingInterval: ts.PollingInterval, - fallbackOptions: ts.WatchOptions | undefined): ts.FileWatcher { - console.log('fswatch') - /*return this.runWithFallbackPolling ? - this.watchFile( - fileOrDirectory, - createFileWatcherCallback(cb), - fallbackPollingInterval, - fallbackOptions - ) :*/ - return createWatcher( - recursive ? this.fsWatchesRecursive : this.fsWatches, - this.toFullPath(fileOrDirectory), - { - directoryName: fileOrDirectory, - cb, - fallbackPollingInterval, - fallbackOptions - } - ); - } - toFullPath(s: string) { - return this.toPath(this.toNormalizedAbsolutePath(s)); - } - toNormalizedAbsolutePath(s: string) { - return ts.getNormalizedAbsolutePath(s, this.currentDirectory); - } - } - function createWatcher(map: ts.MultiMap, path: ts.Path, callback: T): ts.FileWatcher { - map.add(path, callback); - return { close: () => map.remove(path, callback) }; - } - /** Copied from virtualFileSystemWithWatch */ - type TimeOutCallback = () => any; - /** Copied from virtualFileSystemWithWatch */ - interface TestFileWatcher { - cb: ts.FileWatcherCallback; - fileName: string; - pollingInterval: ts.PollingInterval; - } - /** Copied from virtualFileSystemWithWatch */ - interface TestFsWatcher { - cb: ts.FsWatchCallback; - directoryName: string; - fallbackPollingInterval: ts.PollingInterval; - fallbackOptions: ts.WatchOptions | undefined; - } - /** Copied from virtualFileSystemWithWatch */ - class Callbacks { - private map: TimeOutCallback[] = []; - private nextId = 1; - getNextId() { - return this.nextId; - } - register(cb: (...args: any[]) => void, args: any[]) { - const timeoutId = this.nextId; - this.nextId++; - this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args); - return timeoutId; - } - unregister(id: any) { - if (typeof id === "number") { - delete this.map[id]; - } - } - count() { - // ?????????????????????????????????????????????????????????????????? - let n = 0; - for (const _ in this.map) { - n++; - } - return n; - } - invoke(invokeKey?: number) { - if (invokeKey) { - this.map[invokeKey](); - delete this.map[invokeKey]; - return; - } - // Note: invoking a callback may result in new callbacks been queued, - // so do not clear the entire callback list regardless. Only remove the - // ones we have invoked. - for (const key in this.map) { - this.map[key](); - delete this.map[key]; - } - } - } - /** + /** * A fake `ts.CompilerHost` that leverages a virtual file system. */ export class CompilerHost implements ts.CompilerHost { @@ -386,8 +224,8 @@ namespace fakes { private readonly _outputsMap: collections.SortedMap; public readonly traces: string[] = []; public readonly shouldAssertInvariants = !Harness.lightMode; - protected _setParentNodes: boolean; - protected _sourceFiles: collections.SortedMap; + private _setParentNodes: boolean; + private _sourceFiles: collections.SortedMap; private _parseConfigHost: ParseConfigHost | undefined; private _newLine: string; @@ -546,7 +384,6 @@ namespace fakes { return parsed; } - } export type ExpectedDiagnosticMessage = [ts.DiagnosticMessage, ...(string | number)[]]; @@ -574,7 +411,7 @@ namespace fakes { export type ExpectedDiagnostic = ExpectedDiagnosticMessage | ExpectedErrorDiagnostic; - export interface SolutionBuilderDiagnostic { + interface SolutionBuilderDiagnostic { kind: DiagnosticKind; diagnostic: ts.Diagnostic; } @@ -606,6 +443,32 @@ ${indentText}${text}`; return text; } + function expectedDiagnosticRelatedInformationToText({ location, ...diagnosticMessage }: ExpectedDiagnosticRelatedInformation) { + const text = expectedDiagnosticMessageChainToText(diagnosticMessage); + if (location) { + const { file, start, length } = location; + return `${file}(${start}:${length}):: ${text}`; + } + return text; + } + + function expectedErrorDiagnosticToText({ relatedInformation, ...diagnosticRelatedInformation }: ExpectedErrorDiagnostic) { + let text = `${DiagnosticKind.Error}!: ${expectedDiagnosticRelatedInformationToText(diagnosticRelatedInformation)}`; + if (relatedInformation) { + for (const kid of relatedInformation) { + text += ` + related:: ${expectedDiagnosticRelatedInformationToText(kid)}`; + } + } + return text; + } + + function expectedDiagnosticToText(errorOrStatus: ExpectedDiagnostic) { + return ts.isArray(errorOrStatus) ? + `${DiagnosticKind.Status}!: ${expectedDiagnosticMessageToText(errorOrStatus)}` : + expectedErrorDiagnosticToText(errorOrStatus); + } + function diagnosticMessageChainToText({ messageText, next}: ts.DiagnosticMessageChain, indent = 0) { let text = indentedText(indent, messageText); if (next) { @@ -662,39 +525,14 @@ ${indentText}${text}`; return sys; } - - function expectedDiagnosticRelatedInformationToText({ location, ...diagnosticMessage }: ExpectedDiagnosticRelatedInformation) { - const text = expectedDiagnosticMessageChainToText(diagnosticMessage); - if (location) { - const { file, start, length } = location; - return `${file}(${start}:${length}):: ${text}`; - } - return text; - } - - function expectedErrorDiagnosticToText({ relatedInformation, ...diagnosticRelatedInformation }: ExpectedErrorDiagnostic) { - let text = `${DiagnosticKind.Error}!: ${expectedDiagnosticRelatedInformationToText(diagnosticRelatedInformation)}`; - if (relatedInformation) { - for (const kid of relatedInformation) { - text += ` - related:: ${expectedDiagnosticRelatedInformationToText(kid)}`; - } - } - return text; - } - - function expectedDiagnosticToText(errorOrStatus: ExpectedDiagnostic) { - return ts.isArray(errorOrStatus) ? - `${DiagnosticKind.Status}!: ${expectedDiagnosticMessageToText(errorOrStatus)}` : - expectedErrorDiagnosticToText(errorOrStatus); - } - export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost { createProgram: ts.CreateProgram; + private constructor(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram) { super(sys, options, setParentNodes); this.createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram; } + static create(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram) { const host = new SolutionBuilderHost(sys, options, setParentNodes, createProgram); patchHostForBuildInfoReadWrite(host.sys); diff --git a/src/harness/vfsUtil.ts b/src/harness/vfsUtil.ts index 592681bd627..5eca1ce30bb 100644 --- a/src/harness/vfsUtil.ts +++ b/src/harness/vfsUtil.ts @@ -1598,4 +1598,23 @@ namespace vfs { } return text; } + + export function iteratePatch(patch: FileSet | undefined): IterableIterator<[string, string]> | null { + // eslint-disable-next-line no-null/no-null + return patch ? Harness.Compiler.iterateOutputs(iteratePatchWorker("", patch)) : null; + } + + function* iteratePatchWorker(dirname: string, container: FileSet): IterableIterator { + for (const name of Object.keys(container)) { + const entry = normalizeFileSetEntry(container[name]); + const file = dirname ? vpath.combine(dirname, name) : name; + if (entry instanceof Directory) { + yield* ts.arrayFrom(iteratePatchWorker(file, entry.files)); + } + else if (entry instanceof File) { + const content = typeof entry.data === "string" ? entry.data : entry.data.toString("utf8"); + yield new documents.TextDocument(file, content); + } + } + } } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 20e95bac1d5..09d406491ed 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -1,16 +1,16 @@ namespace ts.TestFSWithWatch { - export function createWatchedSystem(fileOrFolderList: readonly vfs.FileOrFolderOrSymLink[], params?: vfs.TestServerHostCreationParameters): TestServerHost { + export function createWatchedSystem(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost { return new TestServerHost(/*withSafelist*/ false, fileOrFolderList, params); } - export function createServerHost(fileOrFolderList: readonly vfs.FileOrFolderOrSymLink[], params?: vfs.TestServerHostCreationParameters): TestServerHost { + export function createServerHost(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost { const host = new TestServerHost(/*withSafelist*/ true, fileOrFolderList, params); // Just like sys, patch the host to use writeFile patchWriteFileEnsuringDirectory(host); return host; } export function verifyMapSize(caption: string, map: ESMap, expectedKeys: readonly string[]) { - assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${vfs.getDiffInKeys(map, expectedKeys)}`); + assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`); } export type MapValueTester = [ESMap | undefined, (value: T) => U]; @@ -147,18 +147,18 @@ namespace ts.TestFSWithWatch { return host; } - export function getTsBuildProjectFile(project: string, file: string): vfs.File { + export function getTsBuildProjectFile(project: string, file: string): File { return { - path: vfs.getTsBuildProjectFilePath(project, file), + path: getTsBuildProjectFilePath(project, file), content: Harness.IO.readFile(`${Harness.IO.getWorkspaceRoot()}/tests/projects/${project}/${file}`)! }; } - export class TestServerHost extends vfs.VirtualServerHost implements server.ServerHost { + export class TestServerHost extends VirtualServerHost implements server.ServerHost { constructor( public withSafeList: boolean, - fileOrFolderorSymLinkList: readonly vfs.FileOrFolderOrSymLink[], - options: vfs.TestServerHostCreationParameters = {}) { + fileOrFolderorSymLinkList: readonly FileOrFolderOrSymLink[], + options: TestServerHostCreationParameters = {}) { super(withSafeList, fileOrFolderorSymLinkList, options); } runQueuedImmediateCallbacks(checkCount?: number) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c451e580cd8..db37d243bef 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -758,7 +758,7 @@ namespace ts.server { readonly toCanonicalFileName: (f: string) => string; public host: ServerHost; - public fs: vfs.VirtualServerHost | undefined; + public fs: ts.TestFSWithWatch.VirtualServerHost | undefined; public readonly logger: Logger; public readonly cancellationToken: HostCancellationToken; public readonly useSingleInferredProject: boolean; @@ -3693,7 +3693,7 @@ namespace ts.server { // 1. set some internal tsserver state for mocked FS (if it hasn't already been set, this might not be the first message) if (!this.fs) { // -nervous laugh- - this.fs = vfs.createVirtualServerHost([]) + this.fs = ts.TestFSWithWatch.createVirtualServerHost([]) ;(this as any).host = this.fs ;(this.session as any).host = this.fs } diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index af1d65a5876..e51664dedb2 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -40,18 +40,15 @@ interface Array { length: number; [n: number]: T; }` return `// some copy right notice ${'content' in file ? file.content : file.fileContent}`; } - function verifyFileSystem(fs: vfs.FileSystem | undefined, files: protocol.FileSystemRequestArgs[]) { + // TODO: This is almost certainly in harness/virtualFileSystemHost.ts, or should be. + function verifyFileSystem(host: ts.TestFSWithWatch.VirtualServerHost | undefined, files: protocol.FileSystemRequestArgs[]) { // 1. make sure that everything in files is there - assert.isDefined(fs) - const result = fs!.scanSync('.', "descendants-or-self", { - accept: (_, stats) => stats.isFile() - }) - assert.equal(result.length, files.length) - let i = 0 + assert.isDefined(host) + const fs = (host as any).fs as ESMap + assert.equal(fs.size, files.length) for (const { file, fileContent } of files) { - assert.equal(result[i], file) - assert.equal(fs!.readFileSync(file, 'utf8'), fileContent) - i++ + assert(host?.fileExists(file)) + assert.equal(host!.readFile(file), fileContent) } // 2. make sure nothing else is } @@ -79,9 +76,9 @@ ${'content' in file ? file.content : file.fileContent}`; }); const service = session.getProjectService(); // session -> service -> project const project = service.configuredProjects.get(config.file)!; - const vfs = (session as any).host.vfs - const fakehost = (session as any).host as fakes.FakeServerHost - assert.isDefined(vfs); + const v = (session as any).host.vfs + const fakehost = (session as any).host as ts.TestFSWithWatch.VirtualServerHost + assert.isDefined(v); assert.isDefined(project); assert.equal(fakehost.fsWatches.size, 0) assert.equal(fakehost.fsWatchesRecursive.size, 0) diff --git a/src/vfs/virtualFileSystemWithWatch.ts b/src/vfs/virtualFileSystemWithWatch.ts index d90c672a716..783338088b2 100644 --- a/src/vfs/virtualFileSystemWithWatch.ts +++ b/src/vfs/virtualFileSystemWithWatch.ts @@ -1,5 +1,5 @@ /* @internal */ -namespace ts.vfs { +namespace ts.TestFSWithWatch { export const libFile: File = { path: "/a/lib/lib.d.ts", content: `/// @@ -85,20 +85,20 @@ interface Array { length: number; [n: number]: T; }` modifiedTime: Date; } - interface FsFile extends FSEntryBase { + export interface FsFile extends FSEntryBase { content: string; fileSize?: number; } - interface FsFolder extends FSEntryBase { + export interface FsFolder extends FSEntryBase { entries: SortedArray; } - interface FsSymLink extends FSEntryBase { + export interface FsSymLink extends FSEntryBase { symLink: string; } - type FSEntry = FsFile | FsFolder | FsSymLink; + export type FSEntry = FsFile | FsFolder | FsSymLink; function isFsFolder(s: FSEntry | undefined): s is FsFolder { return !!s && isArray((s as FsFolder).entries);