Skip to content

Commit ea02ed3

Browse files
Add basic functional tests for the high-level FileSystem class. (#9170)
(for #8995) This adds back in high-level functional tests reverted in #8970, as well as adding some more. There is also a little bit of cleanup (group methods in FileSystem and IFileSystem).
1 parent 1b8a603 commit ea02ed3

File tree

12 files changed

+1915
-272
lines changed

12 files changed

+1915
-272
lines changed

src/client/common/platform/fileSystem.ts

Lines changed: 196 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,131 @@ import { inject, injectable } from 'inversify';
1010
import * as path from 'path';
1111
import * as tmp from 'tmp';
1212
import { promisify } from 'util';
13-
import { FileStat } from 'vscode';
1413
import { createDeferred } from '../utils/async';
1514
import { noop } from '../utils/misc';
16-
import { IFileSystem, IPlatformService, TemporaryFile } from './types';
15+
import { FileStat, FileType, IFileSystem, IPlatformService, TemporaryFile } from './types';
1716

1817
const globAsync = promisify(glob);
1918

19+
// This helper function determines the file type of the given stats
20+
// object. The type follows the convention of node's fs module, where
21+
// a file has exactly one type. Symlinks are not resolved.
22+
function convertFileType(stat: fs.Stats): FileType {
23+
if (stat.isFile()) {
24+
return FileType.File;
25+
} else if (stat.isDirectory()) {
26+
return FileType.Directory;
27+
} else if (stat.isSymbolicLink()) {
28+
// The caller is responsible for combining this ("logical or")
29+
// with File or Directory as necessary.
30+
return FileType.SymbolicLink;
31+
} else {
32+
return FileType.Unknown;
33+
}
34+
}
35+
36+
async function getFileType(filename: string): Promise<FileType> {
37+
let stat: fs.Stats;
38+
try {
39+
// Note that we used to use stat() here instead of lstat().
40+
// This shouldn't matter because the only consumers were
41+
// internal methods that have been updated appropriately.
42+
stat = await fs.lstat(filename);
43+
} catch {
44+
return FileType.Unknown;
45+
}
46+
if (!stat.isSymbolicLink()) {
47+
return convertFileType(stat);
48+
}
49+
50+
// For symlinks we emulate the behavior of the vscode.workspace.fs API.
51+
// See: https://code.visualstudio.com/api/references/vscode-api#FileType
52+
try {
53+
stat = await fs.stat(filename);
54+
} catch {
55+
return FileType.SymbolicLink;
56+
}
57+
if (stat.isFile()) {
58+
return FileType.SymbolicLink | FileType.File;
59+
} else if (stat.isDirectory()) {
60+
return FileType.SymbolicLink | FileType.Directory;
61+
} else {
62+
return FileType.SymbolicLink;
63+
}
64+
}
65+
66+
export function convertStat(old: fs.Stats, filetype: FileType): FileStat {
67+
return {
68+
type: filetype,
69+
size: old.size,
70+
// FileStat.ctime and FileStat.mtime only have 1-millisecond
71+
// resolution, while node provides nanosecond resolution. So
72+
// for now we round to the nearest integer.
73+
// See: https://github.com/microsoft/vscode/issues/84526
74+
ctime: Math.round(old.ctimeMs),
75+
mtime: Math.round(old.mtimeMs)
76+
};
77+
}
78+
2079
@injectable()
2180
export class FileSystem implements IFileSystem {
22-
constructor(@inject(IPlatformService) private platformService: IPlatformService) {}
81+
constructor(
82+
@inject(IPlatformService) private platformService: IPlatformService
83+
) { }
84+
85+
//=================================
86+
// path-related
2387

2488
public get directorySeparatorChar(): string {
2589
return path.sep;
2690
}
91+
92+
public arePathsSame(path1: string, path2: string): boolean {
93+
path1 = path.normalize(path1);
94+
path2 = path.normalize(path2);
95+
if (this.platformService.isWindows) {
96+
return path1.toUpperCase() === path2.toUpperCase();
97+
} else {
98+
return path1 === path2;
99+
}
100+
}
101+
102+
public getRealPath(filePath: string): Promise<string> {
103+
return new Promise<string>(resolve => {
104+
fs.realpath(filePath, (err, realPath) => {
105+
resolve(err ? filePath : realPath);
106+
});
107+
});
108+
}
109+
110+
//=================================
111+
// "raw" operations
112+
27113
public async stat(filePath: string): Promise<FileStat> {
28114
// Do not import vscode directly, as this isn't available in the Debugger Context.
29115
// If stat is used in debugger context, it will fail, however theres a separate PR that will resolve this.
30116
// tslint:disable-next-line: no-require-imports
31117
const vscode = require('vscode');
118+
// Note that, prior to the November release of VS Code,
119+
// stat.ctime was always 0.
120+
// See: https://github.com/microsoft/vscode/issues/84525
32121
return vscode.workspace.fs.stat(vscode.Uri.file(filePath));
33122
}
34-
35-
public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean> {
36-
return new Promise<boolean>(resolve => {
37-
fs.stat(filePath, (error, stats) => {
38-
if (error) {
39-
return resolve(false);
40-
}
41-
return resolve(statCheck(stats));
42-
});
43-
});
123+
public async lstat(filename: string): Promise<FileStat> {
124+
const stat = await fs.lstat(filename);
125+
// Note that, unlike stat(), lstat() does not include the type
126+
// of the symlink's target.
127+
const fileType = convertFileType(stat);
128+
return convertStat(stat, fileType);
44129
}
45130

46-
public fileExists(filePath: string): Promise<boolean> {
47-
return this.objectExists(filePath, stats => stats.isFile());
48-
}
49-
public fileExistsSync(filePath: string): boolean {
50-
return fs.existsSync(filePath);
51-
}
52-
/**
53-
* Reads the contents of the file using utf8 and returns the string contents.
54-
* @param {string} filePath
55-
* @returns {Promise<string>}
56-
* @memberof FileSystem
57-
*/
131+
// Return the UTF8-decoded text of the file.
58132
public readFile(filePath: string): Promise<string> {
59133
return fs.readFile(filePath, 'utf8');
60134
}
135+
public readFileSync(filePath: string): string {
136+
return fs.readFileSync(filePath, 'utf8');
137+
}
61138
public readData(filePath: string): Promise<Buffer> {
62139
return fs.readFile(filePath);
63140
}
@@ -66,10 +143,6 @@ export class FileSystem implements IFileSystem {
66143
await fs.writeFile(filePath, data, options);
67144
}
68145

69-
public directoryExists(filePath: string): Promise<boolean> {
70-
return this.objectExists(filePath, stats => stats.isDirectory());
71-
}
72-
73146
public createDirectory(directoryPath: string): Promise<void> {
74147
return fs.mkdirp(directoryPath);
75148
}
@@ -80,79 +153,20 @@ export class FileSystem implements IFileSystem {
80153
return deferred.promise;
81154
}
82155

83-
public async listdir(root: string): Promise<string[]> {
84-
return new Promise<string[]>(resolve => {
85-
// Now look for Interpreters in this directory
86-
fs.readdir(root, (err, names) => {
87-
if (err) {
88-
return resolve([]);
89-
}
90-
resolve(names.map(name => path.join(root, name)));
91-
});
92-
});
93-
}
94-
95-
public getSubDirectories(rootDir: string): Promise<string[]> {
96-
return new Promise<string[]>(resolve => {
97-
fs.readdir(rootDir, async (error, files) => {
98-
if (error) {
99-
return resolve([]);
100-
}
101-
const subDirs = (await Promise.all(
102-
files.map(async name => {
103-
const fullPath = path.join(rootDir, name);
104-
try {
105-
if ((await fs.stat(fullPath)).isDirectory()) {
106-
return fullPath;
107-
}
108-
// tslint:disable-next-line:no-empty
109-
} catch (ex) {}
110-
})
111-
)).filter(dir => dir !== undefined) as string[];
112-
resolve(subDirs);
156+
public async listdir(dirname: string): Promise<[string, FileType][]> {
157+
const files = await fs.readdir(dirname);
158+
const promises = files
159+
.map(async basename => {
160+
const filename = path.join(dirname, basename);
161+
const fileType = await getFileType(filename);
162+
return [filename, fileType] as [string, FileType];
113163
});
114-
});
115-
}
116-
117-
public async getFiles(rootDir: string): Promise<string[]> {
118-
const files = await fs.readdir(rootDir);
119-
return files.filter(async f => {
120-
const fullPath = path.join(rootDir, f);
121-
if ((await fs.stat(fullPath)).isFile()) {
122-
return true;
123-
}
124-
return false;
125-
});
126-
}
127-
128-
public arePathsSame(path1: string, path2: string): boolean {
129-
path1 = path.normalize(path1);
130-
path2 = path.normalize(path2);
131-
if (this.platformService.isWindows) {
132-
return path1.toUpperCase() === path2.toUpperCase();
133-
} else {
134-
return path1 === path2;
135-
}
164+
return Promise.all(promises);
136165
}
137166

138167
public appendFile(filename: string, data: {}): Promise<void> {
139168
return fs.appendFile(filename, data);
140169
}
141-
public appendFileSync(filename: string, data: {}, encoding: string): void;
142-
public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void;
143-
// tslint:disable-next-line:unified-signatures
144-
public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void;
145-
public appendFileSync(filename: string, data: {}, optionsOrEncoding: {}): void {
146-
return fs.appendFileSync(filename, data, optionsOrEncoding);
147-
}
148-
149-
public getRealPath(filePath: string): Promise<string> {
150-
return new Promise<string>(resolve => {
151-
fs.realpath(filePath, (err, realPath) => {
152-
resolve(err ? filePath : realPath);
153-
});
154-
});
155-
}
156170

157171
public copyFile(src: string, dest: string): Promise<void> {
158172
const deferred = createDeferred<void>();
@@ -177,6 +191,90 @@ export class FileSystem implements IFileSystem {
177191
return deferred.promise;
178192
}
179193

194+
public chmod(filePath: string, mode: string | number): Promise<void> {
195+
return new Promise<void>((resolve, reject) => {
196+
fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException | null) => {
197+
if (err) {
198+
return reject(err);
199+
}
200+
resolve();
201+
});
202+
});
203+
}
204+
205+
public async move(src: string, tgt: string) {
206+
await fs.rename(src, tgt);
207+
}
208+
209+
public createReadStream(filePath: string): fileSystem.ReadStream {
210+
return fileSystem.createReadStream(filePath);
211+
}
212+
213+
public createWriteStream(filePath: string): fileSystem.WriteStream {
214+
return fileSystem.createWriteStream(filePath);
215+
}
216+
217+
//=================================
218+
// utils
219+
220+
public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean> {
221+
return new Promise<boolean>(resolve => {
222+
// Note that we are using stat() rather than lstat(). This
223+
// means that any symlinks are getting resolved.
224+
fs.stat(filePath, (error, stats) => {
225+
if (error) {
226+
return resolve(false);
227+
}
228+
return resolve(statCheck(stats));
229+
});
230+
});
231+
}
232+
public fileExists(filePath: string): Promise<boolean> {
233+
return this.objectExists(filePath, stats => stats.isFile());
234+
}
235+
public fileExistsSync(filePath: string): boolean {
236+
return fs.existsSync(filePath);
237+
}
238+
public directoryExists(filePath: string): Promise<boolean> {
239+
return this.objectExists(filePath, stats => stats.isDirectory());
240+
}
241+
242+
public async getSubDirectories(dirname: string): Promise<string[]> {
243+
let files: [string, FileType][];
244+
try {
245+
files = await this.listdir(dirname);
246+
} catch {
247+
// We're only preserving pre-existng behavior here...
248+
return [];
249+
}
250+
return files
251+
.filter(([_file, fileType]) => {
252+
// We preserve the pre-existing behavior of following
253+
// symlinks.
254+
return (fileType & FileType.Directory) > 0;
255+
})
256+
.map(([filename, _ft]) => filename);
257+
}
258+
public async getFiles(dirname: string): Promise<string[]> {
259+
let files: [string, FileType][];
260+
try {
261+
files = await this.listdir(dirname);
262+
} catch (err) {
263+
// This matches what getSubDirectories() does.
264+
if (!await fs.pathExists(dirname)) {
265+
return [];
266+
}
267+
throw err; // re-throw
268+
}
269+
return files
270+
.filter(([_file, fileType]) => {
271+
// We preserve the pre-existing behavior of following
272+
// symlinks.
273+
return (fileType & FileType.File) > 0;
274+
})
275+
.map(([filename, _ft]) => filename);
276+
}
277+
180278
public getFileHash(filePath: string): Promise<string> {
181279
return new Promise<string>((resolve, reject) => {
182280
fs.lstat(filePath, (err, stats) => {
@@ -191,6 +289,7 @@ export class FileSystem implements IFileSystem {
191289
});
192290
});
193291
}
292+
194293
public async search(globPattern: string, cwd?: string): Promise<string[]> {
195294
let found: string[];
196295
if (cwd) {
@@ -203,6 +302,7 @@ export class FileSystem implements IFileSystem {
203302
}
204303
return Array.isArray(found) ? found : [];
205304
}
305+
206306
public createTemporaryFile(extension: string): Promise<TemporaryFile> {
207307
return new Promise<TemporaryFile>((resolve, reject) => {
208308
tmp.file({ postfix: extension }, (err, tmpFile, _, cleanupCallback) => {
@@ -214,33 +314,6 @@ export class FileSystem implements IFileSystem {
214314
});
215315
}
216316

217-
public createReadStream(filePath: string): fileSystem.ReadStream {
218-
return fileSystem.createReadStream(filePath);
219-
}
220-
221-
public createWriteStream(filePath: string): fileSystem.WriteStream {
222-
return fileSystem.createWriteStream(filePath);
223-
}
224-
225-
public chmod(filePath: string, mode: string): Promise<void> {
226-
return new Promise<void>((resolve, reject) => {
227-
fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException | null) => {
228-
if (err) {
229-
return reject(err);
230-
}
231-
resolve();
232-
});
233-
});
234-
}
235-
236-
public readFileSync(filePath: string): string {
237-
return fs.readFileSync(filePath, 'utf8');
238-
}
239-
240-
public async move(src: string, tgt: string) {
241-
await fs.rename(src, tgt);
242-
}
243-
244317
public async isDirReadonly(dirname: string): Promise<boolean> {
245318
const filePath = `${dirname}${path.sep}___vscpTest___`;
246319
return new Promise<boolean>(resolve => {

0 commit comments

Comments
 (0)