Skip to content

Commit 1708ab9

Browse files
Add unit tests (and make dependencies explicit).
1 parent ff4e768 commit 1708ab9

File tree

3 files changed

+1193
-51
lines changed

3 files changed

+1193
-51
lines changed

src/client/common/platform/fileSystem.ts

Lines changed: 109 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ import * as fs from 'fs';
88
import * as fsextra from 'fs-extra';
99
import * as glob from 'glob';
1010
import { injectable } from 'inversify';
11-
import * as path from 'path';
11+
import * as fspath from 'path';
1212
import * as tmp from 'tmp';
1313
import * as vscode from 'vscode';
1414
import { createDeferred } from '../utils/async';
1515
import { getOSType, OSType } from '../utils/platform';
1616
import {
1717
FileStat, FileType,
18-
IFileSystem, IFileSystemUtils, IRawFileSystem,
18+
IFileSystem, IFileSystemPath, IFileSystemUtils, IRawFileSystem,
1919
TemporaryFile, WriteStream
2020
} from './types';
2121

22+
// tslint:disable:max-classes-per-file
23+
2224
const ENCODING: string = 'utf8';
2325

2426
function getFileType(stat: FileStat): FileType {
@@ -46,7 +48,7 @@ interface IRawFS {
4648
}
4749

4850
interface IRawFSExtra {
49-
chmod(filePath: string, mode: string): Promise<void>;
51+
chmod(filePath: string, mode: string | number): Promise<void>;
5052
readFile(path: string, encoding: string): Promise<string>;
5153
//tslint:disable-next-line:no-any
5254
writeFile(path: string, data: any, options: any): Promise<void>;
@@ -56,6 +58,7 @@ interface IRawFSExtra {
5658
mkdirp(dirname: string): Promise<void>;
5759
rmdir(dirname: string): Promise<void>;
5860
readdir(dirname: string): Promise<string[]>;
61+
remove(dirname: string): Promise<void>;
5962

6063
// non-async
6164
statSync(filename: string): fsextra.Stats;
@@ -67,7 +70,7 @@ interface IRawFSExtra {
6770
// Later we will drop "FileSystem", switching usage to
6871
// "FileSystemUtils" and then rename "RawFileSystem" to "FileSystem".
6972

70-
class RawFileSystem {
73+
export class RawFileSystem implements IRawFileSystem {
7174
constructor(
7275
private readonly nodefs: IRawFS = fs,
7376
private readonly fsExtra: IRawFSExtra = fsextra
@@ -88,18 +91,23 @@ class RawFileSystem {
8891
}
8992

9093
public async mkdirp(dirname: string): Promise<void> {
91-
return this.fsExtra.mkdirp(dirname);
94+
return this.fsExtra.mkdirp(dirname)
95+
.catch(_err => {
96+
//throw err;
97+
});
9298
}
9399

94100
public async rmtree(dirname: string): Promise<void> {
95-
return this.fsExtra.rmdir(dirname);
101+
return this.fsExtra.stat(dirname)
102+
.then(() => this.fsExtra.remove(dirname));
103+
//.catch((err) => this.fsExtra.rmdir(dirname));
96104
}
97105

98106
public async rmfile(filename: string): Promise<void> {
99107
return this.fsExtra.unlink(filename);
100108
}
101109

102-
public async chmod(filename: string, mode: string): Promise<void> {
110+
public async chmod(filename: string, mode: string | number): Promise<void> {
103111
return this.fsExtra.chmod(filename, mode);
104112
}
105113

@@ -111,8 +119,18 @@ class RawFileSystem {
111119
return this.fsExtra.lstat(filename);
112120
}
113121

114-
public async listdir(dirname: string): Promise<string[]> {
115-
return this.fsExtra.readdir(dirname);
122+
// Once we move to the VS Code API, this method becomes a trivial
123+
// wrapper and The "path" parameter can go away.
124+
public async listdir(dirname: string, path: IFileSystemPath): Promise<[string, FileType][]> {
125+
const names: string[] = await this.fsExtra.readdir(dirname);
126+
const promises = names
127+
.map(name => {
128+
const filename = path.join(dirname, name);
129+
return this.lstat(filename)
130+
.then(stat => [name, getFileType(stat)] as [string, FileType])
131+
.catch(() => [name, FileType.Unknown] as [string, FileType]);
132+
});
133+
return Promise.all(promises);
116134
}
117135

118136
public async copyFile(src: string, dest: string): Promise<void> {
@@ -168,13 +186,49 @@ class RawFileSystem {
168186
}
169187
}
170188

171-
// more aliases (to cause less churn)
189+
interface INodePath {
190+
join(...filenames: string[]): string;
191+
normalize(filename: string): string;
192+
}
193+
194+
// Eventually we will merge PathUtils into FileSystemPath.
195+
196+
export class FileSystemPath implements IFileSystemPath {
197+
constructor(
198+
private readonly isWindows = (getOSType() === OSType.Windows),
199+
private readonly raw: INodePath = fspath
200+
) { }
201+
202+
public join(...filenames: string[]): string {
203+
return this.raw.join(...filenames);
204+
}
205+
206+
public normCase(filename: string): string {
207+
filename = this.raw.normalize(filename);
208+
return this.isWindows ? filename.toUpperCase() : filename;
209+
}
210+
}
211+
212+
// We *could* use ICryptUtils, but it's a bit overkill.
213+
function getHashString(data: string): string {
214+
const hash = createHash('sha512')
215+
.update(data);
216+
return hash.digest('hex');
217+
}
218+
219+
type GlobCallback = (err: Error | null, matches: string[]) => void;
220+
//tslint:disable-next-line:no-any
221+
type TempCallback = (err: any, path: string, fd: number, cleanupCallback: () => void) => void;
222+
172223
@injectable()
173224
export class FileSystemUtils implements IFileSystemUtils {
174225
constructor(
175-
private readonly isWindows = (getOSType() === OSType.Windows),
176-
//public readonly raw: IFileSystem = {}
177-
public readonly raw: IRawFileSystem = new RawFileSystem()
226+
public readonly raw: IRawFileSystem = new RawFileSystem(),
227+
public readonly path: IFileSystemPath = new FileSystemPath(),
228+
private readonly getHash = getHashString,
229+
// tslint:disable-next-line:no-unnecessary-callback-wrapper
230+
private readonly globFile = ((pat: string, cb: GlobCallback) => glob(pat, cb)),
231+
private readonly makeTempFile = ((s: string, cb: TempCallback) => tmp.file({ postfix: s }, cb))
178232
) { }
179233

180234
//****************************
@@ -196,13 +250,12 @@ export class FileSystemUtils implements IFileSystemUtils {
196250
// helpers
197251

198252
public arePathsSame(path1: string, path2: string): boolean {
199-
path1 = path.normalize(path1);
200-
path2 = path.normalize(path2);
201-
if (this.isWindows) {
202-
return path1.toUpperCase() === path2.toUpperCase();
203-
} else {
204-
return path1 === path2;
253+
if (path1 === path2) {
254+
return true;
205255
}
256+
path1 = this.path.normCase(path1);
257+
path2 = this.path.normCase(path2);
258+
return path1 === path2;
206259
}
207260

208261
public async pathExists(
@@ -212,7 +265,7 @@ export class FileSystemUtils implements IFileSystemUtils {
212265
let stat: FileStat;
213266
try {
214267
stat = await this.raw.stat(filename);
215-
} catch {
268+
} catch (err) {
216269
return false;
217270
}
218271
if (fileType === undefined) {
@@ -231,7 +284,7 @@ export class FileSystemUtils implements IFileSystemUtils {
231284
public async directoryExists(dirname: string): Promise<boolean> {
232285
return this.pathExists(dirname, FileType.Directory);
233286
}
234-
public fileExistsSync(filename: string): boolean {
287+
public pathExistsSync(filename: string): boolean {
235288
try {
236289
this.raw.statSync(filename);
237290
} catch {
@@ -240,56 +293,47 @@ export class FileSystemUtils implements IFileSystemUtils {
240293
return true;
241294
}
242295

243-
public async listdir(
244-
dirname: string
245-
): Promise<[string, FileType][]> {
246-
const filenames: string[] = await (
247-
this.raw.listdir(dirname)
248-
.then(names => names.map(name => path.join(dirname, name)))
249-
.catch(() => [])
250-
);
251-
const promises = filenames
252-
.map(filename => (
253-
this.raw.stat(filename)
254-
.then(stat => [filename, getFileType(stat)] as [string, FileType])
255-
.catch(() => [filename, FileType.Unknown] as [string, FileType])
256-
));
257-
return Promise.all(promises);
296+
public async listdir(dirname: string): Promise<[string, FileType][]> {
297+
try {
298+
return await this.raw.listdir(dirname, this.path);
299+
} catch {
300+
return [];
301+
}
258302
}
259303
public async getSubDirectories(dirname: string): Promise<string[]> {
260304
return (await this.listdir(dirname))
261-
.filter(([_filename, fileType]) => fileType === FileType.Directory)
262-
.map(([filename, _fileType]) => filename);
305+
.filter(([_name, fileType]) => fileType === FileType.Directory)
306+
.map(([name, _fileType]) => this.path.join(dirname, name));
263307
}
264308
public async getFiles(dirname: string): Promise<string[]> {
265309
return (await this.listdir(dirname))
266-
.filter(([_filename, fileType]) => fileType === FileType.File)
267-
.map(([filename, _fileType]) => filename);
310+
.filter(([_name, fileType]) => fileType === FileType.File)
311+
.map(([name, _fileType]) => this.path.join(dirname, name));
268312
}
269313

270314
public async isDirReadonly(dirname: string): Promise<boolean> {
271315
// Alternative: use tmp.file().
272-
const filename = path.join(dirname, '___vscpTest___');
316+
const filename = this.path.join(dirname, '___vscpTest___');
273317
try {
274318
await this.raw.touch(filename);
275319
} catch {
276-
return false;
320+
await this.raw.stat(dirname); // fails if does not exist
321+
return true;
277322
}
278323
await this.raw.rmfile(filename);
279-
return true;
324+
return false;
280325
}
281326

282327
public async getFileHash(filename: string): Promise<string> {
283328
const stat = await this.raw.lstat(filename);
284-
const hash = createHash('sha512')
285-
.update(`${stat.ctimeMs}-${stat.mtimeMs}`);
286-
return hash.digest('hex');
329+
const data = `${stat.ctimeMs}-${stat.mtimeMs}`;
330+
return this.getHash(data);
287331
}
288332

289333
public async search(globPattern: string): Promise<string[]> {
290334
// We could use util.promisify() here.
291335
return new Promise<string[]>((resolve, reject) => {
292-
glob(globPattern, (ex, files) => {
336+
this.globFile(globPattern, (ex, files) => {
293337
if (ex) {
294338
return reject(ex);
295339
}
@@ -301,7 +345,7 @@ export class FileSystemUtils implements IFileSystemUtils {
301345
public async createTemporaryFile(suffix: string): Promise<TemporaryFile> {
302346
// We could use util.promisify() here.
303347
return new Promise<TemporaryFile>((resolve, reject) => {
304-
tmp.file({ postfix: suffix }, (err, tmpFile, _, cleanupCallback) => {
348+
this.makeTempFile(suffix, (err, tmpFile, _, cleanupCallback) => {
305349
if (err) {
306350
return reject(err);
307351
}
@@ -314,8 +358,21 @@ export class FileSystemUtils implements IFileSystemUtils {
314358
}
315359
}
316360

361+
// more aliases (to cause less churn)
317362
@injectable()
318363
export class FileSystem extends FileSystemUtils implements IFileSystem {
364+
constructor(
365+
isWindows: boolean = (getOSType() === OSType.Windows)
366+
) {
367+
super(
368+
new RawFileSystem(),
369+
new FileSystemPath(isWindows)
370+
);
371+
}
372+
373+
//****************************
374+
// aliases
375+
319376
public async stat(filePath: string): Promise<vscode.FileStat> {
320377
// Do not import vscode directly, as this isn't available in the Debugger Context.
321378
// If stat is used in debugger context, it will fail, however theres a separate PR that will resolve this.
@@ -340,6 +397,10 @@ export class FileSystem extends FileSystemUtils implements IFileSystem {
340397
return this.raw.copyFile(src, dest);
341398
}
342399

400+
public fileExistsSync(filename: string): boolean {
401+
return this.pathExistsSync(filename);
402+
}
403+
343404
public readFileSync(filename: string): string {
344405
return this.raw.readTextSync(filename);
345406
}

src/client/common/platform/types.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export type WriteStream = fs.WriteStream;
5151
export interface IRawFileSystem {
5252
stat(filename: string): Promise<FileStat>;
5353
lstat(filename: string): Promise<FileStat>;
54-
chmod(filename: string, mode: string): Promise<void>;
54+
chmod(filename: string, mode: string | number): Promise<void>;
5555
// files
5656
readText(filename: string): Promise<string>;
5757
writeText(filename: string, data: {}): Promise<void>;
@@ -61,16 +61,24 @@ export interface IRawFileSystem {
6161
// directories
6262
mkdirp(dirname: string): Promise<void>;
6363
rmtree(dirname: string): Promise<void>;
64-
listdir(dirname: string): Promise<string[]>;
64+
listdir(dirname: string, path: IFileSystemPath): Promise<[string, FileType][]>;
6565
// not async
6666
statSync(filename: string): FileStat;
6767
readTextSync(filename: string): string;
6868
createWriteStream(filename: string): WriteStream;
6969
}
7070

71+
// Eventually we will merge IPathUtils into IFileSystemPath.
72+
73+
export interface IFileSystemPath {
74+
join(...filenames: string[]): string;
75+
normCase(filename: string): string;
76+
}
77+
7178
export const IFileSystemUtils = Symbol('IFileSystemUtils');
7279
export interface IFileSystemUtils {
7380
raw: IRawFileSystem;
81+
path: IFileSystemPath;
7482
// aliases
7583
createDirectory(dirname: string): Promise<void>;
7684
deleteDirectory(dirname: string): Promise<void>;
@@ -87,7 +95,7 @@ export interface IFileSystemUtils {
8795
createTemporaryFile(suffix: string): Promise<TemporaryFile>;
8896
// helpers (non-async)
8997
arePathsSame(path1: string, path2: string): boolean; // Move to IPathUtils.
90-
fileExistsSync(filename: string): boolean;
98+
pathExistsSync(filename: string): boolean;
9199
}
92100

93101
// more aliases (to cause less churn)
@@ -99,6 +107,7 @@ export interface IFileSystem extends IFileSystemUtils {
99107
chmod(filename: string, mode: string): Promise<void>;
100108
copyFile(src: string, dest: string): Promise<void>;
101109
// non-async
110+
fileExistsSync(filename: string): boolean;
102111
readFileSync(filename: string): string;
103112
createWriteStream(filename: string): WriteStream;
104113
}

0 commit comments

Comments
 (0)