diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 3ed20df830f4..2d8f5d25e4c6 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -10,54 +10,131 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import * as tmp from 'tmp'; import { promisify } from 'util'; -import { FileStat } from 'vscode'; import { createDeferred } from '../utils/async'; import { noop } from '../utils/misc'; -import { IFileSystem, IPlatformService, TemporaryFile } from './types'; +import { FileStat, FileType, IFileSystem, IPlatformService, TemporaryFile } from './types'; const globAsync = promisify(glob); +// This helper function determines the file type of the given stats +// object. The type follows the convention of node's fs module, where +// a file has exactly one type. Symlinks are not resolved. +function convertFileType(stat: fs.Stats): FileType { + if (stat.isFile()) { + return FileType.File; + } else if (stat.isDirectory()) { + return FileType.Directory; + } else if (stat.isSymbolicLink()) { + // The caller is responsible for combining this ("logical or") + // with File or Directory as necessary. + return FileType.SymbolicLink; + } else { + return FileType.Unknown; + } +} + +async function 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 fs.lstat(filename); + } catch { + return FileType.Unknown; + } + 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 fs.stat(filename); + } catch { + return FileType.SymbolicLink; + } + if (stat.isFile()) { + return FileType.SymbolicLink | FileType.File; + } else if (stat.isDirectory()) { + return FileType.SymbolicLink | FileType.Directory; + } else { + return FileType.SymbolicLink; + } +} + +export function convertStat(old: fs.Stats, filetype: FileType): FileStat { + return { + type: filetype, + size: old.size, + // FileStat.ctime and FileStat.mtime only have 1-millisecond + // resolution, while node provides nanosecond resolution. So + // for now we round to the nearest integer. + // See: https://github.com/microsoft/vscode/issues/84526 + ctime: Math.round(old.ctimeMs), + mtime: Math.round(old.mtimeMs) + }; +} + @injectable() export class FileSystem implements IFileSystem { - constructor(@inject(IPlatformService) private platformService: IPlatformService) {} + constructor( + @inject(IPlatformService) private platformService: IPlatformService + ) { } + + //================================= + // path-related public get directorySeparatorChar(): string { return path.sep; } + + public arePathsSame(path1: string, path2: string): boolean { + path1 = path.normalize(path1); + path2 = path.normalize(path2); + if (this.platformService.isWindows) { + return path1.toUpperCase() === path2.toUpperCase(); + } else { + return path1 === path2; + } + } + + public getRealPath(filePath: string): Promise { + return new Promise(resolve => { + fs.realpath(filePath, (err, realPath) => { + resolve(err ? filePath : realPath); + }); + }); + } + + //================================= + // "raw" operations + 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 vscode = require('vscode'); + // Note that, prior to the November release of VS Code, + // stat.ctime was always 0. + // See: https://github.com/microsoft/vscode/issues/84525 return vscode.workspace.fs.stat(vscode.Uri.file(filePath)); } - - public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise { - return new Promise(resolve => { - fs.stat(filePath, (error, stats) => { - if (error) { - return resolve(false); - } - return resolve(statCheck(stats)); - }); - }); + public async lstat(filename: string): Promise { + const stat = await fs.lstat(filename); + // Note that, unlike stat(), lstat() does not include the type + // of the symlink's target. + const fileType = convertFileType(stat); + return convertStat(stat, fileType); } - public fileExists(filePath: string): Promise { - return this.objectExists(filePath, stats => stats.isFile()); - } - public fileExistsSync(filePath: string): boolean { - return fs.existsSync(filePath); - } - /** - * Reads the contents of the file using utf8 and returns the string contents. - * @param {string} filePath - * @returns {Promise} - * @memberof FileSystem - */ + // Return the UTF8-decoded text of the file. public readFile(filePath: string): Promise { return fs.readFile(filePath, 'utf8'); } + public readFileSync(filePath: string): string { + return fs.readFileSync(filePath, 'utf8'); + } public readData(filePath: string): Promise { return fs.readFile(filePath); } @@ -66,10 +143,6 @@ export class FileSystem implements IFileSystem { await fs.writeFile(filePath, data, options); } - public directoryExists(filePath: string): Promise { - return this.objectExists(filePath, stats => stats.isDirectory()); - } - public createDirectory(directoryPath: string): Promise { return fs.mkdirp(directoryPath); } @@ -80,79 +153,20 @@ export class FileSystem implements IFileSystem { return deferred.promise; } - public async listdir(root: string): Promise { - return new Promise(resolve => { - // Now look for Interpreters in this directory - fs.readdir(root, (err, names) => { - if (err) { - return resolve([]); - } - resolve(names.map(name => path.join(root, name))); - }); - }); - } - - public getSubDirectories(rootDir: string): Promise { - return new Promise(resolve => { - fs.readdir(rootDir, async (error, files) => { - if (error) { - return resolve([]); - } - const subDirs = (await Promise.all( - files.map(async name => { - const fullPath = path.join(rootDir, name); - try { - if ((await fs.stat(fullPath)).isDirectory()) { - return fullPath; - } - // tslint:disable-next-line:no-empty - } catch (ex) {} - }) - )).filter(dir => dir !== undefined) as string[]; - resolve(subDirs); + public async listdir(dirname: string): Promise<[string, FileType][]> { + const files = await fs.readdir(dirname); + const promises = files + .map(async basename => { + const filename = path.join(dirname, basename); + const fileType = await getFileType(filename); + return [filename, fileType] as [string, FileType]; }); - }); - } - - public async getFiles(rootDir: string): Promise { - const files = await fs.readdir(rootDir); - return files.filter(async f => { - const fullPath = path.join(rootDir, f); - if ((await fs.stat(fullPath)).isFile()) { - return true; - } - return false; - }); - } - - public arePathsSame(path1: string, path2: string): boolean { - path1 = path.normalize(path1); - path2 = path.normalize(path2); - if (this.platformService.isWindows) { - return path1.toUpperCase() === path2.toUpperCase(); - } else { - return path1 === path2; - } + return Promise.all(promises); } public appendFile(filename: string, data: {}): Promise { return fs.appendFile(filename, data); } - public appendFileSync(filename: string, data: {}, encoding: string): void; - public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; - // tslint:disable-next-line:unified-signatures - public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; - public appendFileSync(filename: string, data: {}, optionsOrEncoding: {}): void { - return fs.appendFileSync(filename, data, optionsOrEncoding); - } - - public getRealPath(filePath: string): Promise { - return new Promise(resolve => { - fs.realpath(filePath, (err, realPath) => { - resolve(err ? filePath : realPath); - }); - }); - } public copyFile(src: string, dest: string): Promise { const deferred = createDeferred(); @@ -177,6 +191,90 @@ export class FileSystem implements IFileSystem { return deferred.promise; } + public chmod(filePath: string, mode: string | number): Promise { + return new Promise((resolve, reject) => { + fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException | null) => { + if (err) { + return reject(err); + } + resolve(); + }); + }); + } + + public async move(src: string, tgt: string) { + await fs.rename(src, tgt); + } + + public createReadStream(filePath: string): fileSystem.ReadStream { + return fileSystem.createReadStream(filePath); + } + + public createWriteStream(filePath: string): fileSystem.WriteStream { + return fileSystem.createWriteStream(filePath); + } + + //================================= + // utils + + public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise { + return new Promise(resolve => { + // Note that we are using stat() rather than lstat(). This + // means that any symlinks are getting resolved. + fs.stat(filePath, (error, stats) => { + if (error) { + return resolve(false); + } + return resolve(statCheck(stats)); + }); + }); + } + public fileExists(filePath: string): Promise { + return this.objectExists(filePath, stats => stats.isFile()); + } + public fileExistsSync(filePath: string): boolean { + return fs.existsSync(filePath); + } + public directoryExists(filePath: string): Promise { + return this.objectExists(filePath, stats => stats.isDirectory()); + } + + public async getSubDirectories(dirname: string): Promise { + let files: [string, FileType][]; + try { + files = await this.listdir(dirname); + } catch { + // We're only preserving pre-existng behavior here... + return []; + } + return files + .filter(([_file, fileType]) => { + // We preserve the pre-existing behavior of following + // symlinks. + return (fileType & FileType.Directory) > 0; + }) + .map(([filename, _ft]) => filename); + } + public async getFiles(dirname: string): Promise { + let files: [string, FileType][]; + try { + files = await this.listdir(dirname); + } catch (err) { + // This matches what getSubDirectories() does. + if (!await fs.pathExists(dirname)) { + return []; + } + throw err; // re-throw + } + return files + .filter(([_file, fileType]) => { + // We preserve the pre-existing behavior of following + // symlinks. + return (fileType & FileType.File) > 0; + }) + .map(([filename, _ft]) => filename); + } + public getFileHash(filePath: string): Promise { return new Promise((resolve, reject) => { fs.lstat(filePath, (err, stats) => { @@ -191,6 +289,7 @@ export class FileSystem implements IFileSystem { }); }); } + public async search(globPattern: string, cwd?: string): Promise { let found: string[]; if (cwd) { @@ -203,6 +302,7 @@ export class FileSystem implements IFileSystem { } return Array.isArray(found) ? found : []; } + public createTemporaryFile(extension: string): Promise { return new Promise((resolve, reject) => { tmp.file({ postfix: extension }, (err, tmpFile, _, cleanupCallback) => { @@ -214,33 +314,6 @@ export class FileSystem implements IFileSystem { }); } - public createReadStream(filePath: string): fileSystem.ReadStream { - return fileSystem.createReadStream(filePath); - } - - public createWriteStream(filePath: string): fileSystem.WriteStream { - return fileSystem.createWriteStream(filePath); - } - - public chmod(filePath: string, mode: string): Promise { - return new Promise((resolve, reject) => { - fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException | null) => { - if (err) { - return reject(err); - } - resolve(); - }); - }); - } - - public readFileSync(filePath: string): string { - return fs.readFileSync(filePath, 'utf8'); - } - - public async move(src: string, tgt: string) { - await fs.rename(src, tgt); - } - public async isDirReadonly(dirname: string): Promise { const filePath = `${dirname}${path.sep}___vscpTest___`; return new Promise(resolve => { diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index cadd7b0ae1a1..95deb9065c01 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; import * as fsextra from 'fs-extra'; import { SemVer } from 'semver'; -import { Disposable, FileStat } from 'vscode'; +import * as vscode from 'vscode'; import { Architecture, OSType } from '../utils/platform'; export enum RegistryHive { @@ -32,43 +32,46 @@ export interface IPlatformService { getVersion(): Promise; } -export type TemporaryFile = { filePath: string } & Disposable; -export type TemporaryDirectory = { path: string } & Disposable; +export type TemporaryFile = { filePath: string } & vscode.Disposable; +export type TemporaryDirectory = { path: string } & vscode.Disposable; +export import FileType = vscode.FileType; +export import FileStat = vscode.FileStat; export type WriteStream = fs.WriteStream; export const IFileSystem = Symbol('IFileSystem'); export interface IFileSystem { + // path-related directorySeparatorChar: string; + arePathsSame(path1: string, path2: string): boolean; + getRealPath(path: string): Promise; + + // "raw" operations stat(filePath: string): Promise; - objectExists(path: string, statCheck: (s: fs.Stats) => boolean): Promise; - fileExists(path: string): Promise; - fileExistsSync(path: string): boolean; - directoryExists(path: string): Promise; createDirectory(path: string): Promise; deleteDirectory(path: string): Promise; - listdir(dirname: string): Promise; - getSubDirectories(rootDir: string): Promise; - getFiles(rootDir: string): Promise; - arePathsSame(path1: string, path2: string): boolean; - readData(filePath: string): Promise; + listdir(dirname: string): Promise<[string, FileType][]>; readFile(filePath: string): Promise; + readData(filePath: string): Promise; writeFile(filePath: string, data: {}, options?: string | fsextra.WriteFileOptions): Promise; - readFileSync(filename: string): string; appendFile(filename: string, data: {}): Promise; - appendFileSync(filename: string, data: {}, encoding: string): void; - appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; - // tslint:disable-next-line:unified-signatures - appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; - getRealPath(path: string): Promise; copyFile(src: string, dest: string): Promise; deleteFile(filename: string): Promise; + chmod(path: string, mode: string | number): Promise; + move(src: string, tgt: string): Promise; + // sync + readFileSync(filename: string): string; + createReadStream(path: string): fs.ReadStream; + createWriteStream(path: string): fs.WriteStream; + + // utils + fileExists(path: string): Promise; + fileExistsSync(path: string): boolean; + directoryExists(path: string): Promise; + getSubDirectories(rootDir: string): Promise; + getFiles(rootDir: string): Promise; getFileHash(filePath: string): Promise; search(globPattern: string, cwd?: string): Promise; createTemporaryFile(extension: string): Promise; - createReadStream(path: string): fs.ReadStream; - createWriteStream(path: string): fs.WriteStream; - chmod(path: string, mode: string): Promise; - move(src: string, tgt: string): Promise; isDirReadonly(dirname: string): Promise; } diff --git a/src/client/debugger/debugAdapter/main.ts b/src/client/debugger/debugAdapter/main.ts index 0403dc5c7905..c078ee92bb8e 100644 --- a/src/client/debugger/debugAdapter/main.ts +++ b/src/client/debugger/debugAdapter/main.ts @@ -9,6 +9,7 @@ if ((Reflect as any).metadata === undefined) { require('reflect-metadata'); } +import * as fsextra from 'fs-extra'; import { Socket } from 'net'; import { EOL } from 'os'; import * as path from 'path'; @@ -20,7 +21,6 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import '../../common/extensions'; import { isNotInstalledError } from '../../common/helpers'; -import { IFileSystem } from '../../common/platform/types'; import { ICurrentProcess, IDisposable, IDisposableRegistry } from '../../common/types'; import { createDeferred, Deferred, sleep } from '../../common/utils/async'; import { noop } from '../../common/utils/misc'; @@ -47,7 +47,10 @@ export class PythonDebugger extends DebugSession { public debugServer?: BaseDebugServer; public client = createDeferred(); private supportsRunInTerminalRequest: boolean = false; - constructor(private readonly serviceContainer: IServiceContainer) { + constructor( + private readonly serviceContainer: IServiceContainer, + private readonly fileExistsSync = fsextra.existsSync + ) { super(false); } public shutdown(): void { @@ -106,8 +109,7 @@ export class PythonDebugger extends DebugSession { } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - const fs = this.serviceContainer.get(IFileSystem); - if ((typeof args.module !== 'string' || args.module.length === 0) && args.program && !fs.fileExistsSync(args.program)) { + if ((typeof args.module !== 'string' || args.module.length === 0) && args.program && !this.fileExistsSync(args.program)) { return this.sendErrorResponse(response, { format: `File does not exist. "${args.program}"`, id: 1 }, undefined, undefined, ErrorDestination.User); } diff --git a/src/client/debugger/debugAdapter/serviceRegistry.ts b/src/client/debugger/debugAdapter/serviceRegistry.ts index f9337be584b9..140c1b0fe0dd 100644 --- a/src/client/debugger/debugAdapter/serviceRegistry.ts +++ b/src/client/debugger/debugAdapter/serviceRegistry.ts @@ -5,9 +5,6 @@ import { Container } from 'inversify'; import { SocketServer } from '../../common/net/socket/socketServer'; -import { FileSystem } from '../../common/platform/fileSystem'; -import { PlatformService } from '../../common/platform/platformService'; -import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { CurrentProcess } from '../../common/process/currentProcess'; import { BufferDecoder } from '../../common/process/decoder'; import { IBufferDecoder, IProcessServiceFactory } from '../../common/process/types'; @@ -37,8 +34,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugStreamProvider, DebugStreamProvider); serviceManager.addSingleton(IProtocolLogger, ProtocolLogger); serviceManager.add(IProtocolParser, ProtocolParser); - serviceManager.addSingleton(IFileSystem, FileSystem); - serviceManager.addSingleton(IPlatformService, PlatformService); serviceManager.addSingleton(ISocketServer, SocketServer); serviceManager.addSingleton(IProtocolMessageWriter, ProtocolMessageWriter); serviceManager.addSingleton(IBufferDecoder, BufferDecoder); diff --git a/src/client/interpreter/locators/helpers.ts b/src/client/interpreter/locators/helpers.ts index 096437ac624f..ed40d44463a4 100644 --- a/src/client/interpreter/locators/helpers.ts +++ b/src/client/interpreter/locators/helpers.ts @@ -8,15 +8,18 @@ import { IPipEnvServiceHelper } from './types'; const CheckPythonInterpreterRegEx = IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; -export function lookForInterpretersInDirectory(pathToCheck: string, fs: IFileSystem): Promise { +export async function lookForInterpretersInDirectory(pathToCheck: string, fs: IFileSystem): Promise { // Technically, we should be able to use fs.getFiles(). However, // that breaks some tests. So we stick with the broader behavior. - return fs.listdir(pathToCheck) - .then(subDirs => subDirs.filter(fileName => CheckPythonInterpreterRegEx.test(path.basename(fileName)))) - .catch(err => { - traceError('Python Extension (lookForInterpretersInDirectory.fs.listdir):', err); - return [] as string[]; - }); + try { + const subDirs = await fs.listdir(pathToCheck); + return subDirs + .map(([filename, _ft]) => filename) + .filter(fileName => CheckPythonInterpreterRegEx.test(path.basename(fileName))); + } catch (err) { + traceError('Python Extension (lookForInterpretersInDirectory.fs.listdir):', err); + return [] as string[]; + } } @injectable() diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts new file mode 100644 index 000000000000..d3759d69c301 --- /dev/null +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -0,0 +1,1314 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length chai-vague-errors + +import { expect, use } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { convertStat, FileSystem } from '../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { FileType, TemporaryFile } from '../../../client/common/platform/types'; +import { sleep } from '../../../client/common/utils/async'; +import { + assertDoesNotExist, assertExists, DOES_NOT_EXIST, fixPath, FSFixture, + SUPPORTS_SYMLINKS, WINDOWS +} from './utils'; + +// tslint:disable:no-require-imports no-var-requires +const assertArrays = require('chai-arrays'); +use(require('chai-as-promised')); +use(assertArrays); + +suite('FileSystem', () => { + let fileSystem: FileSystem; + let fix: FSFixture; + setup(async () => { + fileSystem = new FileSystem( + new PlatformService() + ); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + await fix.ensureDeleted(DOES_NOT_EXIST); + }); + + suite('path-related', () => { + suite('directorySeparatorChar', () => { + test('value', () => { + const sep = fileSystem.directorySeparatorChar; + + expect(sep).to.equal(path.sep); + }); + }); + + suite('arePathsSame', () => { + test('identical', () => { + const filename = 'x/y/z/spam.py'; + + const result = fileSystem.arePathsSame(filename, filename); + + expect(result).to.equal(true); + }); + + test('not the same', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'a/b/c/spam.py'; + + const result = fileSystem.arePathsSame(file1, file2); + + expect(result).to.equal(false); + }); + + test('with different separators', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'x\\y\\z\\spam.py'; + const expected = WINDOWS; + + const result = fileSystem.arePathsSame(file1, file2); + + expect(result).to.equal(expected); + }); + + test('with different case', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'x/Y/z/Spam.py'; + const expected = WINDOWS; + + const result = fileSystem.arePathsSame(file1, file2); + + expect(result).to.equal(expected); + }); + }); + + suite('getRealPath', () => { + const prevCwd = process.cwd(); + let cwd: string; + setup(async () => { + cwd = await fix.createDirectory('x/y/z'); + process.chdir(cwd); + }); + teardown(() => { + process.chdir(prevCwd); + }); + + test('basename-only', async () => { + const expected = await fix.createFile('x/y/z/spam.py'); + + const resolved = await fileSystem.getRealPath('spam.py'); + + expect(resolved).to.equal(expected); + }); + + test('absolute', async () => { + const filename = await fix.createFile('spam.py'); + const expected = filename; + + const resolved = await fileSystem.getRealPath(filename); + + expect(resolved).to.equal(expected); + }); + + test('relative', async () => { + const expected = await fix.createFile('x/y/z/w/spam.py'); + const relpath = fixPath('./w/spam.py'); + + const resolved = await fileSystem.getRealPath(relpath); + + expect(resolved).to.equal(expected); + }); + + test('parent', async () => { + const expected = await fix.resolve('x/y'); + + const resolved = await fileSystem.getRealPath('..'); + + expect(resolved).to.equal(expected); + }); + + test('cousin', async () => { + const expected = await fix.createFile('x/w/spam.py'); + const relpath = fixPath('../../w/spam.py'); + + const resolved = await fileSystem.getRealPath(relpath); + + expect(resolved).to.equal(expected); + }); + + test('does not exist', async () => { + const resolved = await fileSystem.getRealPath('spam.py'); + + // The original path was returned unchanged. + expect(resolved).to.equal('spam.py'); // instead of /x/y/z/spam.py + }); + + test('directory does not exist', async () => { + const relpath = fixPath('../../w/spam.py'); + + const resolved = await fileSystem.getRealPath(relpath); + + // The original path was returned unchanged. + expect(resolved).to.equal(relpath); // instead of /x/w/spam.py + }); + + test('symlink', async function() { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const expected = await fix.createFile('spam.py'); + await fix.createSymlink('x/y/z/eggs.py', expected); + + const resolved = await fileSystem.getRealPath('eggs.py'); + + expect(resolved).to.equal(expected); + }); + + test('symlink chain', async function() { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const expected = await fix.createFile('w/spam.py'); + const symlink1 = await fix.createSymlink('x/y/spam.py', expected); + await fix.createSymlink('x/y/z/eggs.py', symlink1); + + const resolved = await fileSystem.getRealPath('eggs.py'); + + expect(resolved).to.equal(expected); + }); + + test('symlink (target does not exist)', async function() { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.resolve('spam.py'); + await fix.createSymlink('x/y/z/eggs.py', filename); + + const resolved = await fileSystem.getRealPath('eggs.py'); + + // The original path was returned unchanged. + expect(resolved).to.equal('eggs.py'); // instead of /spam.py + }); + + test('mixed', async function() { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const expected = await fix.createFile('x/y/w/eggs.py'); + await fix.createSymlink('x/w/spam.py', expected); + const relpath = fixPath('../../w/spam.py'); + + const resolved = await fileSystem.getRealPath(relpath); + + expect(resolved).to.equal(expected); + }); + }); + }); + + suite('raw', () => { + suite('lstat', () => { + test('for symlinks, gives the link info', async function() { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const expected = convertStat( + await fs.lstat(symlink), + FileType.SymbolicLink + ); + + const stat = await fileSystem.lstat(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('for normal files, gives the file info', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + // Ideally we would compare to the result of + // fileSystem.stat(). However, we do not have access + // to the VS Code API here. + const expected = convertStat( + await fs.lstat(filename), + FileType.File + ); + + const stat = await fileSystem.lstat(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + const promise = fileSystem.lstat(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('createDirectory', () => { + 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.createDirectory(dirname); + + await assertExists(dirname); + }); + + test('works if the directory already exists', async () => { + const dirname = await fix.createDirectory('spam'); + await assertExists(dirname); + + await fileSystem.createDirectory(dirname); + + await assertExists(dirname); + }); + }); + + suite('deleteDirectory', () => { + test('deletes the directory if empty', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await fileSystem.deleteDirectory(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.deleteDirectory(dirname); + + await expect(promise).to.eventually.be.rejected; + }); + + test('fails if the directory does not exist', async () => { + const promise = fileSystem.deleteDirectory(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('listdir', () => { + 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', '