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/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 25886aabc12d..24f26a1f13d5 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -4,7 +4,6 @@ 'use strict'; import { createHash } from 'crypto'; -import * as fs from 'fs'; import * as fsextra from 'fs-extra'; import * as glob from 'glob'; import { injectable } from 'inversify'; @@ -12,7 +11,6 @@ import * as fspath from 'path'; import * as tmpMod from 'tmp'; import * as util from 'util'; import * as vscode from 'vscode'; -import { createDeferred } from '../utils/async'; import { getOSType, OSType } from '../utils/platform'; import { FileStat, FileType, @@ -22,25 +20,42 @@ import { } from './types'; // tslint:disable:max-classes-per-file +// tslint:disable:no-suspicious-comment const ENCODING: string = 'utf8'; -// Determine the file type from the given file info. -function getFileType(stat: FileStat): FileType { +const FILE_NOT_FOUND = vscode.FileSystemError.FileNotFound().name; +const FILE_EXISTS = vscode.FileSystemError.FileExists().name; + +function isFileNotFoundError(err: Error): boolean { + return err.name === FILE_NOT_FOUND; +} + +function isFileExistsError(err: Error): boolean { + return err.name === FILE_EXISTS; +} + +function convertFileStat(stat: fsextra.Stats): FileStat { + let fileType = FileType.Unknown; if (stat.isFile()) { - return FileType.File; + fileType = FileType.File; } else if (stat.isDirectory()) { - return FileType.Directory; + fileType = FileType.Directory; } else if (stat.isSymbolicLink()) { - return FileType.SymbolicLink; - } else { - return FileType.Unknown; - } + fileType = FileType.SymbolicLink; + } + return { + type: fileType, + size: stat.size, + ctime: stat.ctimeMs, + mtime: stat.mtimeMs + }; } // The parts of node's 'path' module used by FileSystemPath. interface INodePath { join(...filenames: string[]): string; + dirname(filename: string): string; normalize(filename: string): string; } @@ -66,6 +81,10 @@ export class FileSystemPaths implements IFileSystemPaths { return this.raw.join(...filenames); } + public dirname(filename: string): string { + return this.raw.dirname(filename); + } + public normCase(filename: string): string { filename = this.raw.normalize(filename); return this.isCaseSensitive ? filename.toUpperCase() : filename; @@ -112,36 +131,32 @@ export class TempFileSystem { } } -// This is the parts of node's 'fs' module that we use in RawFileSystem. -interface IRawFS { - // non-async - createWriteStream(filePath: string): fs.WriteStream; +// This is the parts of the vscode.workspace.fs API that we use here. +// 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; + 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 { chmod(filePath: string, mode: string | number): Promise; - readFile(path: string, encoding: string): Promise; - //tslint:disable-next-line:no-any - writeFile(path: string, data: any, options: any): Promise; - unlink(filename: string): Promise; - stat(filename: string): Promise; lstat(filename: string): Promise; - mkdirp(dirname: string): Promise; - rmdir(dirname: string): Promise; - readdir(dirname: string): Promise; - remove(dirname: string): Promise; // non-async statSync(filename: string): fsextra.Stats; readFileSync(path: string, encoding: string): string; - createReadStream(src: string): fsextra.ReadStream; - createWriteStream(dest: string): fsextra.WriteStream; + createWriteStream(filePath: string): fsextra.WriteStream; } -// The parts of IFileSystemPaths used by RawFileSystem. interface IRawPath { - join(...filenames: string[]): string; + dirname(filename: string): string; } // Later we will drop "FileSystem", switching usage to @@ -150,105 +165,155 @@ interface IRawPath { // The low-level filesystem operations used by the extension. export class RawFileSystem implements IRawFileSystem { constructor( - protected readonly path: IRawPath, - protected readonly nodefs: IRawFS, + protected readonly paths: IRawPath, + protected readonly vscfs: IVSCodeFileSystemAPI, protected readonly fsExtra: IRawFSExtra ) { } // Create a new object using common-case default values. - public static withDefaults(): RawFileSystem{ + public static withDefaults( + paths?: IRawPath, + vscfs?: IVSCodeFileSystemAPI, + fsExtra?: IRawFSExtra + ): RawFileSystem{ return new RawFileSystem( - FileSystemPaths.withDefaults(), - fs, - fsextra + paths || FileSystemPaths.withDefaults(), + vscfs || vscode.workspace.fs, + fsExtra || fsextra ); } //**************************** - // fs-extra + // VS Code API public async readText(filename: string): Promise { - return this.fsExtra.readFile(filename, ENCODING); + const uri = vscode.Uri.file(filename); + const data = Buffer.from( + await this.vscfs.readFile(uri)); + return data.toString(ENCODING); } - public async writeText(filename: string, data: {}): Promise { - const options: fsextra.WriteFileOptions = { - encoding: ENCODING - }; - await this.fsExtra.writeFile(filename, data, options); - } - - public async mkdirp(dirname: string): Promise { - return this.fsExtra.mkdirp(dirname); + public async writeText(filename: string, text: string): Promise { + const uri = vscode.Uri.file(filename); + const data = Buffer.from(text); + await this.vscfs.writeFile(uri, data); } public async rmtree(dirname: string): Promise { - return this.fsExtra.stat(dirname) - .then(() => this.fsExtra.remove(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 (at least for remote-over-SSH). + // So 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 rmfile(filename: string): Promise { - return this.fsExtra.unlink(filename); - } - - public async chmod(filename: string, mode: string | number): Promise { - return this.fsExtra.chmod(filename, mode); + const uri = vscode.Uri.file(filename); + return this.vscfs.delete(uri, { + recursive: false, + useTrash: false + }); } public async stat(filename: string): Promise { - return this.fsExtra.stat(filename); + const uri = vscode.Uri.file(filename); + return this.vscfs.stat(uri); } - public async lstat(filename: string): Promise { - return this.fsExtra.lstat(filename); + public async listdir(dirname: string): Promise<[string, FileType][]> { + const uri = vscode.Uri.file(dirname); + return this.vscfs.readDirectory(uri); } - // Once we move to the VS Code API, this method becomes a trivial wrapper. - public async listdir(dirname: string): Promise<[string, FileType][]> { - const names: string[] = await this.fsExtra.readdir(dirname); - const promises = names - .map(name => { - const filename = this.path.join(dirname, name); - return this.lstat(filename) - .then(stat => [name, getFileType(stat)] as [string, FileType]) - .catch(() => [name, FileType.Unknown] as [string, FileType]); - }); - return Promise.all(promises); + public async mkdirp(dirname: string): Promise { + // TODO https://github.com/microsoft/vscode/issues/84175 + // Hopefully VS Code provides this method in their API + // so we don't have to roll our own. + const stack = [dirname]; + while (stack.length > 0) { + const current = stack.pop() || ''; + const uri = vscode.Uri.file(current); + try { + await this.vscfs.createDirectory(uri); + } catch (err) { + if (isFileExistsError(err)) { + // already done! + return; + } + // According to the docs, FileNotFound means the parent + // does not exist. + // See: https://code.visualstudio.com/api/references/vscode-api#FileSystemProvider + if (!isFileNotFoundError(err)) { + // Fail for anything else. + throw err; // re-throw + } + // Try creating the parent first. + const parent = this.paths.dirname(current); + if (parent === '' || parent === current) { + // This shouldn't ever happen. + throw err; + } + stack.push(current); + stack.push(parent); + } + } } - // Once we move to the VS Code API, this method becomes a trivial wrapper. public async copyFile(src: string, dest: string): Promise { - const deferred = createDeferred(); - const rs = this.fsExtra.createReadStream(src) - .on('error', (err) => { - deferred.reject(err); - }); - 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); + // TODO (https://github.com/microsoft/vscode/issues/84177) + // The docs say "throws - FileNotFound when parent of + // destination doesn't exist". However, it happily creates + // the parent directory (at least for remote-over-SSH). + // So we have to manually stat, just to be sure. + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(dest))); + await this.vscfs.copy(srcURI, destURI, { + overwrite: true + }); + } + + //**************************** + // fs-extra + + public async chmod(filename: string, mode: string | number): Promise { + // TODO https://github.com/microsoft/vscode/issues/84175 + // This functionality has been requested for the VS Code API. + return this.fsExtra.chmod(filename, mode); + } + + public async lstat(filename: string): Promise { + // TODO https://github.com/microsoft/vscode/issues/84175 + // This functionality has been requested for the VS Code API. + const stat = await this.fsExtra.lstat(filename); + return convertFileStat(stat); } //**************************** // non-async (fs-extra) public statSync(filename: string): FileStat { - return this.fsExtra.statSync(filename); + // TODO https://github.com/microsoft/vscode/issues/84175 + // This functionality has been requested for the VS Code API. + const stat = this.fsExtra.statSync(filename); + return convertFileStat(stat); } public readTextSync(filename: string): string { + // TODO https://github.com/microsoft/vscode/issues/84175 + // This functionality has been requested for the VS Code API. return this.fsExtra.readFileSync(filename, ENCODING); } - //**************************** - // non-async (fs) - public createWriteStream(filename: string): WriteStream { - return this.nodefs.createWriteStream(filename); + // TODO https://github.com/microsoft/vscode/issues/84175 + // This functionality has been requested for the VS Code API. + return this.fsExtra.createWriteStream(filename); } } @@ -257,20 +322,26 @@ export class RawFileSystem implements IRawFileSystem { export class FileSystemUtils implements IFileSystemUtils { constructor( public readonly raw: IRawFileSystem, - public readonly path: IFileSystemPaths, + public readonly paths: IFileSystemPaths, public readonly tmp: ITempFileSystem, protected readonly getHash: (data: string) => string, protected readonly globFile: (pat: string) => Promise ) { } // Create a new object using common-case default values. - public static withDefaults(): FileSystemUtils { - const paths = FileSystemPaths.withDefaults(); + public static withDefaults( + raw?: IRawFileSystem, + paths?: IFileSystemPaths, + tmp?: ITempFileSystem, + getHash?: (data: string) => string, + globFile?: (pat: string) => Promise + ): FileSystemUtils { + paths = paths || FileSystemPaths.withDefaults(); return new FileSystemUtils( - new RawFileSystem(paths, fs, fsextra), + raw || RawFileSystem.withDefaults(paths), paths, - TempFileSystem.withDefaults(), - getHashString, - util.promisify(glob) + tmp || TempFileSystem.withDefaults(), + getHash || getHashString, + globFile || util.promisify(glob) ); } @@ -296,8 +367,8 @@ export class FileSystemUtils implements IFileSystemUtils { if (path1 === path2) { return true; } - path1 = this.path.normCase(path1); - path2 = this.path.normCase(path2); + path1 = this.paths.normCase(path1); + path2 = this.paths.normCase(path2); return path1 === path2; } @@ -307,19 +378,15 @@ export class FileSystemUtils implements IFileSystemUtils { ): Promise { let stat: FileStat; try { - stat = await this.raw.stat(filename); + stat = await this._stat(filename); } catch (err) { return false; } + if (fileType === undefined) { return true; - } else if (fileType === FileType.File) { - return stat.isFile(); - } else if (fileType === FileType.Directory) { - return stat.isDirectory(); - } else { - return false; } + return stat.type === fileType; } public async fileExists(filename: string): Promise { return this.pathExists(filename, FileType.File); @@ -338,12 +405,12 @@ export class FileSystemUtils implements IFileSystemUtils { public async getSubDirectories(dirname: string): Promise { return (await this.listdir(dirname)) .filter(([_name, fileType]) => fileType === FileType.Directory) - .map(([name, _fileType]) => this.path.join(dirname, name)); + .map(([name, _fileType]) => this.paths.join(dirname, name)); } public async getFiles(dirname: string): Promise { return (await this.listdir(dirname)) .filter(([_name, fileType]) => fileType === FileType.File) - .map(([name, _fileType]) => this.path.join(dirname, name)); + .map(([name, _fileType]) => this.paths.join(dirname, name)); } public async isDirReadonly(dirname: string): Promise { @@ -352,7 +419,7 @@ export class FileSystemUtils implements IFileSystemUtils { tmpFile = await this.tmp.createFile('___vscpTest___', dirname); } catch { // Use a stat call to ensure the directory exists. - await this.raw.stat(dirname); + await this._stat(dirname); return true; } tmpFile.dispose(); @@ -361,7 +428,7 @@ export class FileSystemUtils implements IFileSystemUtils { public async getFileHash(filename: string): Promise { const stat = await this.raw.lstat(filename); - const data = `${stat.ctimeMs}-${stat.mtimeMs}`; + const data = `${stat.ctime}-${stat.mtime}`; return this.getHash(data); } @@ -369,6 +436,10 @@ export class FileSystemUtils implements IFileSystemUtils { const files = await this.globFile(globPattern); return Array.isArray(files) ? files : []; } + + public async _stat(filename: string): Promise { + return this.raw.stat(filename); + } } // We *could* use ICryptoUtils, but it's a bit overkill, issue tracked @@ -382,7 +453,7 @@ function getHashString(data: string): string { // more aliases (to cause less churn) @injectable() export class FileSystem implements IFileSystem { - private readonly utils: FileSystemUtils; + protected utils: FileSystemUtils; constructor() { this.utils = FileSystemUtils.withDefaults(); } @@ -442,12 +513,8 @@ export class FileSystem implements IFileSystem { //**************************** // aliases - public async stat(filePath: string): Promise { - // Do not import vscode directly, as this isn't available in the Debugger Context. - // If stat is used in debugger context, it will fail, however theres a separate PR that will resolve this. - // tslint:disable-next-line: no-require-imports - const vsc = require('vscode'); - return vsc.workspace.fs.stat(vscode.Uri.file(filePath)); + public async stat(filePath: string): Promise { + return this.utils._stat(filePath); } public async readFile(filename: string): Promise { diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index 317540932e99..880f87b90175 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -4,7 +4,6 @@ 'use strict'; import * as fs from 'fs'; -import * as fsextra from 'fs-extra'; import { SemVer } from 'semver'; import * as vscode from 'vscode'; import { Architecture, OSType } from '../utils/platform'; @@ -48,11 +47,12 @@ export interface ITempFileSystem { export interface IFileSystemPaths { join(...filenames: string[]): string; + dirname(filename: string): string; normCase(filename: string): string; } export import FileType = vscode.FileType; -export type FileStat = fsextra.Stats; +export type FileStat = vscode.FileStat; export type WriteStream = fs.WriteStream; // Later we will drop "IFileSystem", switching usage to @@ -104,7 +104,7 @@ export interface IRawFileSystem { export const IFileSystemUtils = Symbol('IFileSystemUtils'); export interface IFileSystemUtils { readonly raw: IRawFileSystem; - readonly path: IFileSystemPaths; + readonly paths: IFileSystemPaths; readonly tmp: ITempFileSystem; //*********************** @@ -157,7 +157,7 @@ export interface IFileSystem { search(globPattern: string): Promise; arePathsSame(path1: string, path2: string): boolean; - stat(filePath: string): Promise; + stat(filePath: string): Promise; readFile(filename: string): Promise; writeFile(filename: string, data: {}): Promise; chmod(filename: string, mode: string): Promise; diff --git a/src/client/workspaceSymbols/provider.ts b/src/client/workspaceSymbols/provider.ts index 019bcd25592d..ee2460be6e8c 100644 --- a/src/client/workspaceSymbols/provider.ts +++ b/src/client/workspaceSymbols/provider.ts @@ -43,7 +43,13 @@ export class WorkspaceSymbolProvider implements IWorspaceSymbolProvider { .filter(generator => generator !== undefined && generator.enabled) .map(async generator => { // load tags - const items = await parseTags(generator!.workspaceFolder.fsPath, generator!.tagFilePath, query, token); + const items = await parseTags( + generator!.workspaceFolder.fsPath, + generator!.tagFilePath, + query, + token, + this.fs + ); if (!Array.isArray(items)) { return []; } diff --git a/src/test/common/crypto.unit.test.ts b/src/test/common/crypto.unit.test.ts index dfa6f1801dcd..e97b5b4adaed 100644 --- a/src/test/common/crypto.unit.test.ts +++ b/src/test/common/crypto.unit.test.ts @@ -4,19 +4,23 @@ 'use strict'; import { assert, expect } from 'chai'; +import * as fsextra 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'; // 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'); setup(() => { crypto = new CryptoUtils(); }); + async function getWordList(): Promise { + const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'randomWords.txt'); + const words = await fsextra.readFile(file, 'utf8'); + return words.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'); @@ -52,8 +56,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 getWordList(); const buckets: number[] = Array(100).fill(0); const hashes = Array(10).fill(0); for (const w of wordList) { @@ -72,8 +75,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 getWordList(); 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 24419c7369d8..74f3e1f18b7e 100644 --- a/src/test/common/platform/filesystem.functional.test.ts +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -24,23 +24,26 @@ const assertArrays = require('chai-arrays'); use(assertArrays); use(chaiAsPromised); -const WINDOWS = /^win/.test(process.platform); +// Note: all functional tests that trigger the VS Code "fs" API are +// found in filesystem.test.ts. -const DOES_NOT_EXIST = 'this file does not exist'; +export const WINDOWS = /^win/.test(process.platform); -async function assertDoesNotExist(filename: string) { +export const DOES_NOT_EXIST = 'this file does not exist'; + +export async function assertDoesNotExist(filename: string) { await expect( fsextra.stat(filename) ).to.eventually.be.rejected; } -async function assertExists(filename: string) { +export async function assertExists(filename: string) { await expect( fsextra.stat(filename) ).to.not.eventually.be.rejected; } -class FSFixture { +export class FSFixture { public tempDir: tmpMod.SynchrounousResult | undefined; public sockServer: net.Server | undefined; @@ -196,138 +199,6 @@ suite('Raw FileSystem', () => { await fix.cleanUp(); }); - 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 fsextra.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); - - const actual = await fsextra.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); - - const actual = await fsextra.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); - - const actual = await fsextra.readFile(filename) - .then(buffer => buffer.toString()); - expect(actual).to.equal(data); - }); - }); - - 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); - - await filesystem.mkdirp(dirname); - - await assertExists(dirname); - }); - }); - - suite('rmtree', () => { - test('deletes the directory and everything in it', async () => { - const dirname = await fix.createDirectory('x'); - const filename = await fix.createFile('x/y/z/spam.py'); - await assertExists(filename); - - await filesystem.rmtree(dirname); - - await assertDoesNotExist(dirname); - }); - - test('fails if the directory does not exist', async () => { - const promise = filesystem.rmtree(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - - suite('rmfile', () => { - test('deletes the file', 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('chmod (non-Windows)', () => { suiteSetup(function () { // On Windows, chmod won't have any effect on the file itself. @@ -336,7 +207,6 @@ suite('Raw FileSystem', () => { this.skip(); } }); - async function checkMode(filename: string, expected: number) { const stat = await fsextra.stat(filename); expect(stat.mode & 0o777).to.equal(expected); @@ -385,47 +255,17 @@ suite('Raw FileSystem', () => { }); }); - suite('stat', () => { - test('gets the info for an existing file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const expected = await fsextra.stat(filename); - - const stat = await filesystem.stat(filename); - - expect(stat).to.deep.equal(expected); - }); - - test('gets the info for an existing directory', async () => { - const dirname = await fix.createDirectory('x/y/z/spam'); - const expected = await fsextra.stat(dirname); - - const stat = await filesystem.stat(dirname); - - expect(stat).to.deep.equal(expected); - }); - - test('for symlinks, gets the info for the linked file', async () => { - const filename = await fix.createFile('x/y/z/spam.py', '...'); - const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); - const expected = await fsextra.stat(filename); - - const stat = await filesystem.stat(symlink); - - expect(stat).to.deep.equal(expected); - }); - - test('fails if the file does not exist', async () => { - const promise = filesystem.stat(DOES_NOT_EXIST); - - await expect(promise).to.eventually.be.rejected; - }); - }); - suite('lstat', () => { test('for symlinks, gives the link info', async () => { const filename = await fix.createFile('x/y/z/spam.py', '...'); const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); - const expected = await fsextra.lstat(symlink); + const old = await fsextra.lstat(symlink); + const expected = { + type: FileType.SymbolicLink, + size: old.size, + ctime: old.ctimeMs, + mtime: old.mtimeMs + }; const stat = await filesystem.lstat(symlink); @@ -434,7 +274,13 @@ suite('Raw FileSystem', () => { test('for normal files, gives the file info', async () => { const filename = await fix.createFile('x/y/z/spam.py', '...'); - const expected = await fsextra.stat(filename); + const old = await fsextra.stat(filename); + const expected = { + type: FileType.File, + size: old.size, + ctime: old.ctimeMs, + mtime: old.mtimeMs + }; const stat = await filesystem.lstat(filename); @@ -448,120 +294,16 @@ suite('Raw FileSystem', () => { }); }); - suite('listdir', () => { - test('mixed', async () => { - // Create the target directory and its contents. - const dirname = await fix.createDirectory('x/y/z'); - await fix.createFile('x/y/z/__init__.py', ''); - const script = await fix.createFile('x/y/z/__main__.py', '