diff --git a/extensions/vscode-api-tests/src/memfs.ts b/extensions/vscode-api-tests/src/memfs.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b4a5f751ab4a28691014c95c0ddd59a41aecafc --- /dev/null +++ b/extensions/vscode-api-tests/src/memfs.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as path from 'path'; +import * as vscode from 'vscode'; + +class File implements vscode.FileStat { + + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + + name: string; + data?: Uint8Array; + + constructor(name: string) { + this.type = vscode.FileType.File; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.name = name; + } +} + +class Directory implements vscode.FileStat { + + type: vscode.FileType; + ctime: number; + mtime: number; + size: number; + + name: string; + entries: Map; + + constructor(name: string) { + this.type = vscode.FileType.Directory; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.name = name; + this.entries = new Map(); + } +} + +export type Entry = File | Directory; + +export class MemFS implements vscode.FileSystemProvider { + + readonly scheme = 'fake-fs'; + + readonly root = new Directory(''); + + // --- manage file metadata + + stat(uri: vscode.Uri): vscode.FileStat { + return this._lookup(uri, false); + } + + readDirectory(uri: vscode.Uri): [string, vscode.FileType][] { + const entry = this._lookupAsDirectory(uri, false); + let result: [string, vscode.FileType][] = []; + for (const [name, child] of entry.entries) { + result.push([name, child.type]); + } + return result; + } + + // --- manage file contents + + readFile(uri: vscode.Uri): Uint8Array { + const data = this._lookupAsFile(uri, false).data; + if (data) { + return data; + } + throw vscode.FileSystemError.FileNotFound(); + } + + writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void { + let basename = path.posix.basename(uri.path); + let parent = this._lookupParentDirectory(uri); + let entry = parent.entries.get(basename); + if (entry instanceof Directory) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (!entry && !options.create) { + throw vscode.FileSystemError.FileNotFound(uri); + } + if (entry && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(uri); + } + if (!entry) { + entry = new File(basename); + parent.entries.set(basename, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + } + entry.mtime = Date.now(); + entry.size = content.byteLength; + entry.data = content; + + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + } + + // --- manage files/folders + + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void { + + if (!options.overwrite && this._lookup(newUri, true)) { + throw vscode.FileSystemError.FileExists(newUri); + } + + let entry = this._lookup(oldUri, false); + let oldParent = this._lookupParentDirectory(oldUri); + + let newParent = this._lookupParentDirectory(newUri); + let newName = path.posix.basename(newUri.path); + + oldParent.entries.delete(entry.name); + entry.name = newName; + newParent.entries.set(newName, entry); + + this._fireSoon( + { type: vscode.FileChangeType.Deleted, uri: oldUri }, + { type: vscode.FileChangeType.Created, uri: newUri } + ); + } + + delete(uri: vscode.Uri): void { + let dirname = uri.with({ path: path.posix.dirname(uri.path) }); + let basename = path.posix.basename(uri.path); + let parent = this._lookupAsDirectory(dirname, false); + if (!parent.entries.has(basename)) { + throw vscode.FileSystemError.FileNotFound(uri); + } + parent.entries.delete(basename); + parent.mtime = Date.now(); + parent.size -= 1; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: dirname }, { uri, type: vscode.FileChangeType.Deleted }); + } + + createDirectory(uri: vscode.Uri): void { + let basename = path.posix.basename(uri.path); + let dirname = uri.with({ path: path.posix.dirname(uri.path) }); + let parent = this._lookupAsDirectory(dirname, false); + + let entry = new Directory(basename); + parent.entries.set(entry.name, entry); + parent.mtime = Date.now(); + parent.size += 1; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: dirname }, { type: vscode.FileChangeType.Created, uri }); + } + + // --- lookup + + private _lookup(uri: vscode.Uri, silent: false): Entry; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined; + private _lookup(uri: vscode.Uri, silent: boolean): Entry | undefined { + let parts = uri.path.split('/'); + let entry: Entry = this.root; + for (const part of parts) { + if (!part) { + continue; + } + let child: Entry | undefined; + if (entry instanceof Directory) { + child = entry.entries.get(part); + } + if (!child) { + if (!silent) { + throw vscode.FileSystemError.FileNotFound(uri); + } else { + return undefined; + } + } + entry = child; + } + return entry; + } + + private _lookupAsDirectory(uri: vscode.Uri, silent: boolean): Directory { + let entry = this._lookup(uri, silent); + if (entry instanceof Directory) { + return entry; + } + throw vscode.FileSystemError.FileNotADirectory(uri); + } + + private _lookupAsFile(uri: vscode.Uri, silent: boolean): File { + let entry = this._lookup(uri, silent); + if (entry instanceof File) { + return entry; + } + throw vscode.FileSystemError.FileIsADirectory(uri); + } + + private _lookupParentDirectory(uri: vscode.Uri): Directory { + const dirname = uri.with({ path: path.posix.dirname(uri.path) }); + return this._lookupAsDirectory(dirname, false); + } + + // --- manage file events + + private _emitter = new vscode.EventEmitter(); + private _bufferedEvents: vscode.FileChangeEvent[] = []; + private _fireSoonHandle?: NodeJS.Timer; + + readonly onDidChangeFile: vscode.Event = this._emitter.event; + + watch(_resource: vscode.Uri): vscode.Disposable { + // ignore, fires for all changes... + return new vscode.Disposable(() => { }); + } + + private _fireSoon(...events: vscode.FileChangeEvent[]): void { + this._bufferedEvents.push(...events); + + if (this._fireSoonHandle) { + clearTimeout(this._fireSoonHandle); + } + + this._fireSoonHandle = setTimeout(() => { + this._emitter.fire(this._bufferedEvents); + this._bufferedEvents.length = 0; + }, 5); + } +} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts index 81c9c2eeadf5bf5934e00d36ae10f376a8829f5c..0a828ad8d736c656b3edc96e51de56657ef7aa41 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { join } from 'path'; import * as vscode from 'vscode'; -import { createRandomFile } from '../utils'; +import { createRandomFile, testFs } from '../utils'; suite('languages namespace tests', () => { @@ -103,7 +103,7 @@ suite('languages namespace tests', () => { return [new vscode.DocumentLink(range, target)]; } }; - vscode.languages.registerDocumentLinkProvider({ language: 'java', scheme: 'file' }, linkProvider); + vscode.languages.registerDocumentLinkProvider({ language: 'java', scheme: testFs.scheme }, linkProvider); const links = await vscode.commands.executeCommand('vscode.executeLinkProvider', doc.uri); assert.equal(2, links && links.length); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 898a41247c3c5b16884310b9fc87ac8008d3e80e..3617b55f41ca6edfa04325ea8e5a9d7445fe600d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -5,10 +5,9 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { createRandomFile, deleteFile, closeAllEditors, pathEquals, rndName, disposeAll } from '../utils'; +import { createRandomFile, deleteFile, closeAllEditors, pathEquals, rndName, disposeAll, testFs } from '../utils'; import { join, posix, basename } from 'path'; import * as fs from 'fs'; -import * as os from 'os'; suite('workspace-namespace', () => { @@ -131,8 +130,7 @@ suite('workspace-namespace', () => { assert.ok(fs.existsSync(path)); d0.dispose(); - - return deleteFile(vscode.Uri.file(join(vscode.workspace.rootPath || '', './newfile.txt'))); + fs.unlinkSync(join(vscode.workspace.rootPath || '', './newfile.txt')); }); }); @@ -702,10 +700,8 @@ suite('workspace-namespace', () => { test('WorkspaceEdit: edit and rename parent folder duplicates resource #42641', async function () { - let dir = join(os.tmpdir(), 'before-' + rndName()); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } + let dir = vscode.Uri.parse(`${testFs.scheme}:/before-${rndName()}`); + await testFs.createDirectory(dir); let docUri = await createRandomFile('', dir); let docParent = docUri.with({ path: posix.dirname(docUri.path) }); diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index d9fda3bb76de8473b4776145610259f5643cc4a5..38ede848840f02a0b194c469f857531efe09d4d0 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -4,25 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as os from 'os'; -import { join } from 'path'; +import { MemFS } from './memfs'; +import * as assert from 'assert'; export function rndName() { return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); } -export function createRandomFile(contents = '', dir: string = os.tmpdir(), ext = ''): Thenable { - return new Promise((resolve, reject) => { - const tmpFile = join(dir, rndName() + ext); - fs.writeFile(tmpFile, contents, (error) => { - if (error) { - return reject(error); - } +export const testFs = new MemFS(); +vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs); - resolve(vscode.Uri.file(tmpFile)); - }); - }); +export async function createRandomFile(contents = '', dir: vscode.Uri | undefined = undefined, ext = ''): Promise { + let fakeFile: vscode.Uri; + if (dir) { + assert.equal(dir.scheme, testFs.scheme); + fakeFile = dir.with({ path: dir.path + '/' + rndName() + ext }); + } else { + fakeFile = vscode.Uri.parse(`${testFs.scheme}:/${rndName() + ext}`); + } + await testFs.writeFile(fakeFile, Buffer.from(contents), { create: true, overwrite: true }); + return fakeFile; +} + +export async function deleteFile(file: vscode.Uri): Promise { + try { + await testFs.delete(file); + return true; + } catch { + return false; + } } export function pathEquals(path1: string, path2: string): boolean { @@ -34,30 +44,13 @@ export function pathEquals(path1: string, path2: string): boolean { return path1 === path2; } -export function deleteFile(file: vscode.Uri): Thenable { - return new Promise((resolve, reject) => { - fs.unlink(file.fsPath, (err) => { - if (err) { - reject(err); - } else { - resolve(true); - } - }); - }); -} - export function closeAllEditors(): Thenable { return vscode.commands.executeCommand('workbench.action.closeAllEditors'); } export function disposeAll(disposables: vscode.Disposable[]) { - while (disposables.length) { - let item = disposables.pop(); - if (item) { - item.dispose(); - } - } + vscode.Disposable.from(...disposables).dispose(); } export function conditionalTest(name: string, testCallback: (done: MochaDone) => void | Thenable) { @@ -73,4 +66,4 @@ export function conditionalTest(name: string, testCallback: (done: MochaDone) => function isTestTypeActive(): boolean { return !!vscode.extensions.getExtension('vscode-resolver-test'); -} \ No newline at end of file +}