diff --git a/news/3 Code Health/6911.md b/news/3 Code Health/6911.md new file mode 100644 index 000000000000..f7a40ecad97d --- /dev/null +++ b/news/3 Code Health/6911.md @@ -0,0 +1 @@ +Use the new VS Code filesystem API as much as possible. diff --git a/src/client/common/platform/errors.ts b/src/client/common/platform/errors.ts index 777d02b7430b..59156152b98f 100644 --- a/src/client/common/platform/errors.ts +++ b/src/client/common/platform/errors.ts @@ -78,6 +78,16 @@ interface ISystemError extends INodeJSError { port?: string; } +// Return a new error for errno ENOTEMPTY. +export function createDirNotEmptyError(dirname: string): ISystemError { + const err = new Error(`directory "${dirname}" not empty`) as ISystemError; + err.name = 'SystemError'; + err.code = 'ENOTEMPTY'; + err.path = dirname; + err.syscall = 'rmdir'; + return err; +} + function isSystemError(err: Error, expectedCode: string): boolean | undefined { const code = (err as ISystemError).code; if (!code) { @@ -130,3 +140,8 @@ export function isNoPermissionsError(err: Error): boolean | undefined { } return isSystemError(err, 'EACCES'); } + +// Return true if the given error is ENOTEMPTY. +export function isDirNotEmptyError(err: Error): boolean | undefined { + return isSystemError(err, 'ENOTEMPTY'); +} diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 10e42aa63521..a622d62f8eb9 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. 'use strict'; +// tslint:disable:no-suspicious-comment + import { createHash } from 'crypto'; import * as fs from 'fs-extra'; import * as glob from 'glob'; @@ -10,8 +12,7 @@ import { promisify } from 'util'; import * as vscode from 'vscode'; import '../../common/extensions'; import { traceError } from '../logger'; -import { createDeferred } from '../utils/async'; -import { isFileNotFoundError, isNoPermissionsError } from './errors'; +import { createDirNotEmptyError, isFileExistsError, isFileNotFoundError, isNoPermissionsError } from './errors'; import { FileSystemPaths, FileSystemPathUtils } from './fs-paths'; import { TemporaryFileSystem } from './fs-temp'; import { @@ -84,31 +85,32 @@ function filterByFileType( // See: https://code.visualstudio.com/api/references/vscode-api#FileSystem // Note that we have used all the API functions *except* "rename()". interface IVSCodeFileSystemAPI { + copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable; + createDirectory(uri: vscode.Uri): Thenable; + delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable; + readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>; + readFile(uri: vscode.Uri): Thenable; + rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable; stat(uri: vscode.Uri): Thenable; + writeFile(uri: vscode.Uri, content: Uint8Array): Thenable; } // This is the parts of the 'fs-extra' module that we use in RawFileSystem. interface IRawFSExtra { - stat(filename: string): Promise; lstat(filename: string): Promise; - readdir(dirname: string): Promise; - readFile(filename: string): Promise; - readFile(filename: string, encoding: string): Promise; - mkdirp(dirname: string): Promise; chmod(filePath: string, mode: string | number): Promise; - rename(src: string, tgt: string): Promise; - writeFile(filename: string, data: {}, options: {}): Promise; appendFile(filename: string, data: {}): Promise; - unlink(filename: string): Promise; - rmdir(dirname: string): Promise; // non-async + lstatSync(filename: string): fs.Stats; + statSync(filename: string): fs.Stats; readFileSync(path: string, encoding: string): string; createReadStream(filename: string): ReadStream; createWriteStream(filename: string): WriteStream; } interface IRawPath { + dirname(path: string): string; join(...paths: string[]): string; } @@ -148,6 +150,8 @@ export class RawFileSystem implements IRawFileSystem { } public async lstat(filename: string): Promise { + // TODO https://github.com/microsoft/vscode/issues/71204 (84514)): + // This functionality has been requested for the VS Code API. const stat = await this.fsExtra.lstat(filename); // Note that, unlike stat(), lstat() does not include the type // of the symlink's target. @@ -156,144 +160,165 @@ export class RawFileSystem implements IRawFileSystem { } public async chmod(filename: string, mode: string | number): Promise { + // TODO (https://github.com/microsoft/vscode/issues/73122 (84513)): + // This functionality has been requested for the VS Code API. return this.fsExtra.chmod(filename, mode); } - public async move(src: string, tgt: string) { - await this.fsExtra.rename(src, tgt); + public async move(src: string, tgt: string): Promise { + const srcUri = vscode.Uri.file(src); + const tgtUri = vscode.Uri.file(tgt); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(tgt))); + // We stick with the pre-existing behavior where files are + // overwritten and directories are not. + const options = { overwrite: false }; + try { + await this.vscfs.rename(srcUri, tgtUri, options); + } catch (err) { + if (!isFileExistsError(err)) { + throw err; // re-throw + } + const stat = await this.vscfs.stat(tgtUri); + if (stat.type === FileType.Directory) { + throw err; // re-throw + } + options.overwrite = true; + await this.vscfs.rename(srcUri, tgtUri, options); + } } public async readData(filename: string): Promise { - return this.fsExtra.readFile(filename); + const uri = vscode.Uri.file(filename); + const data = await this.vscfs.readFile(uri); + return Buffer.from(data); } public async readText(filename: string): Promise { - return this.fsExtra.readFile(filename, ENCODING); + const uri = vscode.Uri.file(filename); + const result = await this.vscfs.readFile(uri); + const data = Buffer.from(result); + return data.toString(ENCODING); } public async writeText(filename: string, text: string): Promise { - await this.fsExtra.writeFile(filename, text, { encoding: ENCODING }); + const uri = vscode.Uri.file(filename); + const data = Buffer.from(text); + await this.vscfs.writeFile(uri, data); } public async appendText(filename: string, text: string): Promise { + // TODO (GH-9900): We *could* use the new API for this. return this.fsExtra.appendFile(filename, text); } public async copyFile(src: string, dest: string): Promise { - const deferred = createDeferred(); - // prettier-ignore - const rs = this.fsExtra.createReadStream(src) - .on('error', err => { - deferred.reject(err); - }); - // prettier-ignore - const ws = this.fsExtra.createWriteStream(dest) - .on('error', err => { - deferred.reject(err); - }) - .on('close', () => { - deferred.resolve(); - }); - rs.pipe(ws); - return deferred.promise; + const srcURI = vscode.Uri.file(src); + const destURI = vscode.Uri.file(dest); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(dest))); + await this.vscfs.copy(srcURI, destURI, { + overwrite: true + }); } public async rmfile(filename: string): Promise { - return this.fsExtra.unlink(filename); + const uri = vscode.Uri.file(filename); + return this.vscfs.delete(uri, { + recursive: false, + useTrash: false + }); + } + + public async rmdir(dirname: string): Promise { + const uri = vscode.Uri.file(dirname); + // The "recursive" option disallows directories, even if they + // are empty. So we have to deal with this ourselves. + const files = await this.vscfs.readDirectory(uri); + if (files && files.length > 0) { + throw createDirNotEmptyError(dirname); + } + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false + }); } public async rmtree(dirname: string): Promise { - return this.fsExtra.rmdir(dirname); + const uri = vscode.Uri.file(dirname); + // TODO (https://github.com/microsoft/vscode/issues/84177): + // The docs say "throws - FileNotFound when uri doesn't exist". + // However, it happily does nothing. So for now we have to + // manually stat, just to be sure. + await this.vscfs.stat(uri); + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false + }); } public async mkdirp(dirname: string): Promise { - return this.fsExtra.mkdirp(dirname); + const uri = vscode.Uri.file(dirname); + await this.vscfs.createDirectory(uri); } public async listdir(dirname: string): Promise<[string, FileType][]> { - const files = await this.fsExtra.readdir(dirname); - const promises = files.map(async basename => { + const uri = vscode.Uri.file(dirname); + const files = await this.vscfs.readDirectory(uri); + return files.map(([basename, filetype]) => { const filename = this.paths.join(dirname, basename); - let fileType: FileType; - try { - // Note that getFileType() follows symlinks (while still - // preserving the Symlink flag). - fileType = await this.getFileType(filename); - } catch (err) { - traceError(`failure while getting file type for "${filename}"`, err); - fileType = FileType.Unknown; - } - return [filename, fileType] as [string, FileType]; + return [filename, filetype] as [string, FileType]; }); - return Promise.all(promises); } //**************************** // non-async + // VS Code has decided to never support any sync functions (aside + // from perhaps create*Stream()). + // See: https://github.com/microsoft/vscode/issues/84518 + + public statSync(filename: string): FileStat { + // We follow the filetype behavior of the VS Code API, by + // acknowledging symlinks. + let stat = this.fsExtra.lstatSync(filename); + let filetype = FileType.Unknown; + if (stat.isSymbolicLink()) { + filetype = FileType.SymbolicLink; + stat = this.fsExtra.statSync(filename); + } + filetype |= convertFileType(stat); + return convertStat(stat, filetype); + } + public readTextSync(filename: string): string { return this.fsExtra.readFileSync(filename, ENCODING); } public createReadStream(filename: string): ReadStream { + // TODO (https://github.com/microsoft/vscode/issues/84515): + // This functionality has been requested for the VS Code API. return this.fsExtra.createReadStream(filename); } public createWriteStream(filename: string): WriteStream { + // TODO (https://github.com/microsoft/vscode/issues/84515): + // This functionality has been requested for the VS Code API. return this.fsExtra.createWriteStream(filename); } - - //**************************** - // internal - - private async getFileType(filename: string): Promise { - let stat: fs.Stats; - try { - // Note that we used to use stat() here instead of lstat(). - // This shouldn't matter because the only consumers were - // internal methods that have been updated appropriately. - stat = await this.fsExtra.lstat(filename); - } catch (err) { - if (isFileNotFoundError(err)) { - return FileType.Unknown; - } - throw err; - } - if (!stat.isSymbolicLink()) { - return convertFileType(stat); - } - - // For symlinks we emulate the behavior of the vscode.workspace.fs API. - // See: https://code.visualstudio.com/api/references/vscode-api#FileType - try { - stat = await this.fsExtra.stat(filename); - } catch (err) { - if (isFileNotFoundError(err)) { - return FileType.SymbolicLink; - } - throw err; - } - if (stat.isFile()) { - return FileType.SymbolicLink | FileType.File; - } else if (stat.isDirectory()) { - return FileType.SymbolicLink | FileType.Directory; - } else { - return FileType.SymbolicLink; - } - } } //========================================== // filesystem "utils" -// This is the parts of the 'fs-extra' module that we use in RawFileSystem. -interface IFSExtraForUtils { - open(path: string, flags: string | number, mode?: string | number | null): Promise; - close(fd: number): Promise; - unlink(path: string): Promise; - existsSync(path: string): boolean; -} - // High-level filesystem operations used by the extension. export class FileSystemUtils implements IFileSystemUtils { constructor( @@ -301,8 +326,6 @@ export class FileSystemUtils implements IFileSystemUtils { public readonly pathUtils: IFileSystemPathUtils, public readonly paths: IFileSystemPaths, public readonly tmp: ITempFileSystem, - // tslint:disable-next-line:no-shadowed-variable - private readonly fs: IFSExtraForUtils, private readonly getHash: (data: string) => string, private readonly globFiles: (pat: string, options?: { cwd: string }) => Promise ) {} @@ -311,7 +334,6 @@ export class FileSystemUtils implements IFileSystemUtils { raw?: IRawFileSystem, pathUtils?: IFileSystemPathUtils, tmp?: ITempFileSystem, - fsDeps?: IFSExtraForUtils, getHash?: (data: string) => string, globFiles?: (pat: string, options?: { cwd: string }) => Promise ): FileSystemUtils { @@ -321,7 +343,6 @@ export class FileSystemUtils implements IFileSystemUtils { pathUtils, pathUtils.paths, tmp || TemporaryFileSystem.withDefaults(), - fsDeps || fs, getHash || getHashString, globFiles || promisify(glob) ); @@ -335,7 +356,7 @@ export class FileSystemUtils implements IFileSystemUtils { } public async deleteDirectory(directoryPath: string): Promise { - return this.raw.rmtree(directoryPath); + return this.raw.rmdir(directoryPath); } public async deleteFile(filename: string): Promise { @@ -407,20 +428,18 @@ export class FileSystemUtils implements IFileSystemUtils { public async isDirReadonly(dirname: string): Promise { const filePath = `${dirname}${this.paths.sep}___vscpTest___`; - const flags = fs.constants.O_CREAT | fs.constants.O_RDWR; - let fd: number; try { - fd = await this.fs.open(filePath, flags); + await this.raw.stat(dirname); + await this.raw.writeText(filePath, ''); } catch (err) { if (isNoPermissionsError(err)) { return true; } throw err; // re-throw } - // Clean resources in the background. - this.fs - .close(fd) - .finally(() => this.fs.unlink(filePath)) + this.raw + .rmfile(filePath) + // Clean resources in the background. .ignoreErrors(); return false; } @@ -449,7 +468,15 @@ export class FileSystemUtils implements IFileSystemUtils { // helpers (non-async) public fileExistsSync(filePath: string): boolean { - return this.fs.existsSync(filePath); + try { + this.raw.statSync(filePath); + } catch (err) { + if (isFileNotFoundError(err)) { + return false; + } + throw err; // re-throw + } + return true; } } diff --git a/src/client/common/platform/fs-temp.ts b/src/client/common/platform/fs-temp.ts index ed9b48e8214c..e3e52bf1bf77 100644 --- a/src/client/common/platform/fs-temp.ts +++ b/src/client/common/platform/fs-temp.ts @@ -5,6 +5,10 @@ import * as tmp from 'tmp'; import { ITempFileSystem, TemporaryFile } from './types'; interface IRawTempFS { + // tslint:disable-next-line:no-suspicious-comment + // TODO (https://github.com/microsoft/vscode/issues/84517) + // This functionality has been requested for the + // VS Code FS API (vscode.workspace.fs.*). // tslint:disable-next-line:no-any file(config: tmp.Options, callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void): void; } diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index 488d8b1e9a0e..ec583df6bbc3 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -126,6 +126,8 @@ export interface IRawFileSystem { // Create the directory and any missing parent directories. mkdirp(dirname: string): Promise; + // Delete the directory if empty. + rmdir(dirname: string): Promise; // Delete the directory and everything in it. rmtree(dirname: string): Promise; // Return the contents of the directory. @@ -134,6 +136,8 @@ export interface IRawFileSystem { //*********************** // not async + // Get information about a file (resolve symlinks). + statSync(filename: string): FileStat; // Return the text of the given file (decoded from UTF-8). readTextSync(filename: string): string; // Create a streaming wrappr around an open file (for reading). diff --git a/src/test/common/crypto.unit.test.ts b/src/test/common/crypto.unit.test.ts index 7ab6e52e00d9..175987c275bf 100644 --- a/src/test/common/crypto.unit.test.ts +++ b/src/test/common/crypto.unit.test.ts @@ -4,19 +4,27 @@ 'use strict'; import { assert, expect } from 'chai'; +import * as fs from 'fs-extra'; import * as path from 'path'; import { CryptoUtils } from '../../client/common/crypto'; -import { FileSystem } from '../../client/common/platform/fileSystem'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; +const RANDOM_WORDS = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'randomWords.txt'); + // tslint:disable-next-line: max-func-body-length suite('Crypto Utils', async () => { let crypto: CryptoUtils; - const fs = new FileSystem(); - const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'randomWords.txt'); + let wordsText: string; + suiteSetup(async () => { + wordsText = await fs.readFile(RANDOM_WORDS, 'utf8'); + }); setup(() => { crypto = new CryptoUtils(); }); + async function getRandomWords(): Promise { + return wordsText.split('\n'); + } + test('If hashFormat equals `number`, method createHash() returns a number', async () => { const hash = crypto.createHash('blabla', 'number'); assert.typeOf(hash, 'number', 'Type should be a number'); @@ -62,8 +70,7 @@ suite('Crypto Utils', async () => { assert.notEqual(hash1, hash2, 'Hashes should be different strings'); }); test('If hashFormat equals `number`, ensure numbers are uniformly distributed on scale from 0 to 100', async () => { - const words = await fs.readFile(file); - const wordList = words.split('\n'); + const wordList = await getRandomWords(); const buckets: number[] = Array(100).fill(0); const hashes = Array(10).fill(0); for (const w of wordList) { @@ -82,8 +89,7 @@ suite('Crypto Utils', async () => { } }); test('If hashFormat equals `number`, on a scale of 0 to 100, small difference in the input on average produce large differences (about 33) in the output ', async () => { - const words = await fs.readFile(file); - const wordList = words.split('\n'); + const wordList = await getRandomWords(); const buckets: number[] = Array(100).fill(0); let hashes: number[] = []; let totalDifference = 0; diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts index fda385686562..2e5b44ad4694 100644 --- a/src/test/common/platform/filesystem.functional.test.ts +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -5,16 +5,15 @@ import { expect, use } from 'chai'; import * as fs from 'fs-extra'; -import * as path from 'path'; import { convertStat, FileSystem, FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; import { FileSystemPaths, FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; import { FileType } from '../../../client/common/platform/types'; import { sleep } from '../../../client/common/utils/async'; // prettier-ignore import { - assertDoesNotExist, assertExists, DOES_NOT_EXIST, - fixPath, FSFixture, - OSX, SUPPORTS_SOCKETS, SUPPORTS_SYMLINKS, WINDOWS + assertDoesNotExist, DOES_NOT_EXIST, + fixPath, FSFixture, OSX, + SUPPORTS_SOCKETS, SUPPORTS_SYMLINKS, WINDOWS } from './utils'; // tslint:disable:no-require-imports no-var-requires @@ -135,253 +134,6 @@ suite('FileSystem - raw', () => { }); }); - suite('move', () => { - test('rename file', async () => { - const source = await fix.createFile('spam.py', ''); - const target = await fix.resolve('eggs-txt'); - await assertDoesNotExist(target); - - await fileSystem.move(source, target); - - await assertExists(target); - const text = await fs.readFile(target, 'utf8'); - expect(text).to.equal(''); - await assertDoesNotExist(source); - }); - - test('rename directory', async () => { - const source = await fix.createDirectory('spam'); - await fix.createFile('spam/data.json', ''); - const target = await fix.resolve('eggs'); - const filename = await fix.resolve('eggs/data.json', false); - await assertDoesNotExist(target); - - await fileSystem.move(source, target); - - await assertExists(filename); - const text = await fs.readFile(filename, 'utf8'); - expect(text).to.equal(''); - await assertDoesNotExist(source); - }); - - test('rename symlink', async function() { - if (!SUPPORTS_SYMLINKS) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - const filename = await fix.createFile('spam.py'); - const symlink = await fix.createSymlink('spam.lnk', filename); - const target = await fix.resolve('eggs'); - await assertDoesNotExist(target); - - await fileSystem.move(symlink, target); - - await assertExists(target); - const linked = await fs.readlink(target); - expect(linked).to.equal(filename); - await assertDoesNotExist(symlink); - }); - - test('move file', async () => { - const source = await fix.createFile('spam.py', ''); - await fix.createDirectory('eggs'); - const target = await fix.resolve('eggs/spam.py'); - await assertDoesNotExist(target); - - await fileSystem.move(source, target); - - await assertExists(target); - const text = await fs.readFile(target, 'utf8'); - expect(text).to.equal(''); - await assertDoesNotExist(source); - }); - - test('move directory', async () => { - const source = await fix.createDirectory('spam/spam/spam/eggs/spam'); - await fix.createFile('spam/spam/spam/eggs/spam/data.json', ''); - await fix.createDirectory('spam/spam/spam/hash'); - const target = await fix.resolve('spam/spam/spam/hash/spam'); - const filename = await fix.resolve('spam/spam/spam/hash/spam/data.json', false); - await assertDoesNotExist(target); - - await fileSystem.move(source, target); - - await assertExists(filename); - const text = await fs.readFile(filename, 'utf8'); - expect(text).to.equal(''); - await assertDoesNotExist(source); - }); - - test('move symlink', async function() { - if (!SUPPORTS_SYMLINKS) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - const filename = await fix.createFile('spam.py'); - const symlink = await fix.createSymlink('w/spam.lnk', filename); - const target = await fix.resolve('x/spam.lnk'); - await assertDoesNotExist(target); - - await fileSystem.move(symlink, target); - - await assertExists(target); - const linked = await fs.readlink(target); - expect(linked).to.equal(filename); - await assertDoesNotExist(symlink); - }); - - test('file target already exists', async () => { - const source = await fix.createFile('spam.py', ''); - const target = await fix.createFile('eggs-txt', ''); - - await fileSystem.move(source, target); - - await assertDoesNotExist(source); - await assertExists(target); - const text2 = await fs.readFile(target, 'utf8'); - expect(text2).to.equal(''); - }); - - test('directory target already exists', async () => { - const source = await fix.createDirectory('spam'); - const file3 = await fix.createFile('spam/data.json', ''); - const target = await fix.createDirectory('eggs'); - const file1 = await fix.createFile('eggs/spam.py', ''); - const file2 = await fix.createFile('eggs/data.json', ''); - - const promise = fileSystem.move(source, target); - - await expect(promise).to.eventually.be.rejected; - // Make sure nothing changed. - const text1 = await fs.readFile(file1, 'utf8'); - expect(text1).to.equal(''); - const text2 = await fs.readFile(file2, 'utf8'); - expect(text2).to.equal(''); - const text3 = await fs.readFile(file3, 'utf8'); - expect(text3).to.equal(''); - }); - - test('fails if the file does not exist', async () => { - const source = await fix.resolve(DOES_NOT_EXIST); - const target = await fix.resolve('spam.py'); - - const promise = fileSystem.move(source, target); - - await expect(promise).to.eventually.be.rejected; - // Make sure nothing changed. - await assertDoesNotExist(target); - }); - - test('fails if the target directory does not exist', async () => { - const source = await fix.createFile('x/spam.py', ''); - const target = await fix.resolve('w/spam.py', false); - await assertDoesNotExist(path.dirname(target)); - - const promise = fileSystem.move(source, target); - - await expect(promise).to.eventually.be.rejected; - // Make sure nothing changed. - await assertExists(source); - await assertDoesNotExist(target); - }); - }); - - suite('readData', () => { - test('returns contents of a file', async () => { - const text = ''; - const expected = Buffer.from(text, 'utf8'); - const filename = await fix.createFile('x/y/z/spam.py', text); - - const content = await fileSystem.readData(filename); - - expect(content).to.deep.equal(expected); - }); - - test('throws an exception if file does not exist', async () => { - const promise = fileSystem.readData(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('readText', () => { - test('returns contents of a file', async () => { - const expected = ''; - const filename = await fix.createFile('x/y/z/spam.py', expected); - - const content = await fileSystem.readText(filename); - - expect(content).to.be.equal(expected); - }); - - test('always UTF-8', async () => { - const expected = '... 😁 ...'; - const filename = await fix.createFile('x/y/z/spam.py', expected); - - const text = await fileSystem.readText(filename); - - expect(text).to.equal(expected); - }); - - test('returns garbage if encoding is UCS-2', async () => { - const filename = await fix.resolve('spam.py'); - // There are probably cases where this would fail too. - // However, the extension never has to deal with non-UTF8 - // cases, so it doesn't matter too much. - const original = '... 😁 ...'; - await fs.writeFile(filename, original, { encoding: 'ucs2' }); - - const text = await fileSystem.readText(filename); - - expect(text).to.equal('.\u0000.\u0000.\u0000 \u0000=�\u0001� \u0000.\u0000.\u0000.\u0000'); - }); - - test('throws an exception if file does not exist', async () => { - const promise = fileSystem.readText(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('writeText', () => { - test('creates the file if missing', async () => { - const filename = await fix.resolve('x/y/z/spam.py'); - await assertDoesNotExist(filename); - const data = 'line1\nline2\n'; - - await fileSystem.writeText(filename, data); - - // prettier-ignore - const actual = await fs.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - - test('always UTF-8', async () => { - const filename = await fix.resolve('x/y/z/spam.py'); - const data = '... 😁 ...'; - - await fileSystem.writeText(filename, data); - - // prettier-ignore - const actual = await fs.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - - test('overwrites existing file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const data = 'line1\nline2\n'; - - await fileSystem.writeText(filename, data); - - // prettier-ignore - const actual = await fs.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - }); - suite('appendText', () => { test('existing file', async () => { const orig = 'spamspamspam\n\n'; @@ -422,291 +174,50 @@ suite('FileSystem - raw', () => { }); }); - suite('copyFile', () => { - test('the source file gets copied (same directory)', async () => { - const data = ''; - const src = await fix.createFile('x/y/z/spam.py', data); - const dest = await fix.resolve('x/y/z/spam.py.bak'); - await assertDoesNotExist(dest); - - await fileSystem.copyFile(src, dest); - - // prettier-ignore - const actual = await fs.readFile(dest) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - // prettier-ignore - const original = await fs.readFile(src) - .then(buffer => buffer.toString()); - expect(original).to.equal(data); - }); - - test('the source file gets copied (different directory)', async () => { - const data = ''; - const src = await fix.createFile('x/y/z/spam.py', data); - const dest = await fix.resolve('x/y/eggs.py'); - await assertDoesNotExist(dest); - - await fileSystem.copyFile(src, dest); - - // prettier-ignore - const actual = await fs.readFile(dest) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - // prettier-ignore - const original = await fs.readFile(src) - .then(buffer => buffer.toString()); - expect(original).to.equal(data); - }); - - test('fails if the source does not exist', async () => { - const dest = await fix.resolve('x/spam.py'); - - const promise = fileSystem.copyFile(DOES_NOT_EXIST, dest); - - await expect(promise).to.eventually.be.rejected; - }); - - test('fails if the target parent directory does not exist', async () => { - const src = await fix.createFile('x/spam.py', '...'); - const dest = await fix.resolve('y/eggs.py', false); - await assertDoesNotExist(path.dirname(dest)); - - const promise = fileSystem.copyFile(src, dest); - - await expect(promise).to.eventually.be.rejected; - }); - }); + // non-async - suite('rmfile', () => { - test('deletes the file', async () => { + suite('statSync', () => { + test('for normal files, gives the file info', async () => { const filename = await fix.createFile('x/y/z/spam.py', '...'); - await assertExists(filename); - - await fileSystem.rmfile(filename); - - await assertDoesNotExist(filename); - }); - - test('fails if the file does not exist', async () => { - const promise = fileSystem.rmfile(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('rmtree', () => { - test('deletes the directory if empty', async () => { - const dirname = await fix.createDirectory('x'); - await assertExists(dirname); - - await fileSystem.rmtree(dirname); - - await assertDoesNotExist(dirname); - }); - - test('fails if the directory is not empty', async () => { - const dirname = await fix.createDirectory('x'); - const filename = await fix.createFile('x/y/z/spam.py'); - await assertExists(filename); - - const promise = fileSystem.rmtree(dirname); - - await expect(promise).to.eventually.be.rejected; - }); - - test('fails if the directory does not exist', async () => { - const promise = fileSystem.rmtree(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('mkdirp', () => { - test('creates the directory and all missing parents', async () => { - await fix.createDirectory('x'); - // x/y, x/y/z, and x/y/z/spam are all missing. - const dirname = await fix.resolve('x/y/z/spam', false); - await assertDoesNotExist(dirname); - - await fileSystem.mkdirp(dirname); - - await assertExists(dirname); - }); - - test('works if the directory already exists', async () => { - const dirname = await fix.createDirectory('spam'); - await assertExists(dirname); + // Ideally we would compare to the result of + // fileSystem.stat(). However, we do not have access + // to the VS Code API here. + // prettier-ignore + const expected = convertStat( + await fs.stat(filename), + FileType.File + ); - await fileSystem.mkdirp(dirname); + const stat = fileSystem.statSync(filename); - await assertExists(dirname); + expect(stat).to.deep.equal(expected); }); - }); - suite('listdir', () => { - setup(function() { - if (WINDOWS) { - // tslint:disable-next-line:no-suspicious-comment - // TODO(GH-8995) These tests are failing on Windows, - // so we are // temporarily disabling it. + test('for symlinks, gives the linked info', async function() { + if (!SUPPORTS_SYMLINKS) { // tslint:disable-next-line:no-invalid-this - return this.skip(); + this.skip(); } - }); - if (SUPPORTS_SYMLINKS) { - test('mixed', async () => { - // Create the target directory and its contents. - const dirname = await fix.createDirectory('x/y/z'); - const file1 = await fix.createFile('x/y/z/__init__.py', ''); - const script = await fix.createFile('x/y/z/__main__.py', '