@zenfs/core 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { Buffer } from 'buffer';
3
+ import assert from 'node:assert/strict';
4
+ import { suite, test } from 'node:test';
5
+ import { config, fs } from '../common.js';
6
+
7
+ suite('Utf8Stream', config('write'), () => {
8
+ test('writes synchronously', config('sync'), () => {
9
+ const stream = new fs.Utf8Stream({ dest: 'u8-sync.log', sync: true });
10
+
11
+ assert.equal(stream.write('hello '), true);
12
+ assert.equal(stream.write('wörld\n'), true);
13
+ assert.equal(fs.readFileSync('u8-sync.log', 'utf8'), 'hello wörld\n');
14
+
15
+ stream.destroy();
16
+ });
17
+
18
+ test('exposes its options', config('sync'), () => {
19
+ const stream = new fs.Utf8Stream({ dest: 'u8-props.log', sync: true, minLength: 16, maxLength: 32 });
20
+
21
+ assert.equal(stream.file, 'u8-props.log');
22
+ assert.equal(stream.contentMode, 'utf8');
23
+ assert.equal(stream.sync, true);
24
+ assert.equal(stream.append, true);
25
+ assert.equal(stream.minLength, 16);
26
+ assert.equal(stream.maxLength, 32);
27
+ assert.equal(stream.periodicFlush, 0);
28
+ assert.equal(stream.mkdir, false);
29
+ assert.ok(stream.fd > 0);
30
+
31
+ stream.destroy();
32
+ });
33
+
34
+ test('buffers until minLength, then flushes', config('sync'), () => {
35
+ const stream = new fs.Utf8Stream({ dest: 'u8-buffered.log', sync: true, minLength: 4096 });
36
+
37
+ stream.write('buffered');
38
+ assert.equal(fs.readFileSync('u8-buffered.log', 'utf8'), '');
39
+
40
+ stream.flushSync();
41
+ assert.equal(fs.readFileSync('u8-buffered.log', 'utf8'), 'buffered');
42
+
43
+ stream.destroy();
44
+ });
45
+
46
+ test('truncates when append is false', config('sync'), () => {
47
+ fs.writeFileSync('u8-append.log', 'existing\n');
48
+
49
+ const stream = new fs.Utf8Stream({ dest: 'u8-append.log', sync: true, append: false });
50
+ stream.write('replaced');
51
+ assert.equal(fs.readFileSync('u8-append.log', 'utf8'), 'replaced');
52
+
53
+ stream.destroy();
54
+ });
55
+
56
+ test('writes buffers when contentMode is buffer', config('sync'), () => {
57
+ const stream = new fs.Utf8Stream({ dest: 'u8-buffer.log', sync: true, contentMode: 'buffer' });
58
+
59
+ assert.equal(stream.contentMode, 'buffer');
60
+ assert.throws(() => stream.write('not a buffer'), { code: 'ERR_INVALID_ARG_TYPE' });
61
+ stream.write(Buffer.from('hi '));
62
+ stream.write(Buffer.from('there'));
63
+ assert.equal(fs.readFileSync('u8-buffer.log', 'utf8'), 'hi there');
64
+
65
+ stream.destroy();
66
+ });
67
+
68
+ test('drops writes over maxLength', config('sync'), () => {
69
+ const stream = new fs.Utf8Stream({ dest: 'u8-drop.log', sync: true, maxLength: 4 });
70
+
71
+ const dropped: string[] = [];
72
+ stream.on('drop', data => dropped.push(data.toString()));
73
+
74
+ stream.write('12345');
75
+ stream.write('12');
76
+
77
+ assert.deepEqual(dropped, ['12345']);
78
+ assert.equal(fs.readFileSync('u8-drop.log', 'utf8'), '12');
79
+
80
+ stream.destroy();
81
+ });
82
+
83
+ test('reopen switches files', config('sync'), () => {
84
+ const stream = new fs.Utf8Stream({ dest: 'u8-rotate.log', sync: true, minLength: 4096 });
85
+
86
+ stream.write('before');
87
+ stream.flushSync();
88
+ stream.reopen('u8-rotated.log');
89
+ assert.equal(stream.file, 'u8-rotated.log');
90
+
91
+ stream.write('after');
92
+ stream.flushSync();
93
+
94
+ assert.equal(fs.readFileSync('u8-rotate.log', 'utf8'), 'before');
95
+ assert.equal(fs.readFileSync('u8-rotated.log', 'utf8'), 'after');
96
+
97
+ stream.destroy();
98
+ });
99
+
100
+ test('creates missing directories when mkdir is set', config('sync', 'directories'), () => {
101
+ const stream = new fs.Utf8Stream({ dest: 'u8-deep/dir/out.log', sync: true, mkdir: true });
102
+
103
+ stream.write('made it');
104
+ assert.equal(fs.readFileSync('u8-deep/dir/out.log', 'utf8'), 'made it');
105
+
106
+ stream.destroy();
107
+ });
108
+
109
+ test('writes to an existing descriptor', config('sync'), () => {
110
+ const fd = fs.openSync('u8-fd.log', 'w');
111
+ const stream = new fs.Utf8Stream({ fd, sync: true });
112
+
113
+ assert.equal(stream.fd, fd);
114
+ stream.write('via fd');
115
+ assert.equal(fs.readFileSync('u8-fd.log', 'utf8'), 'via fd');
116
+
117
+ stream.destroy();
118
+ });
119
+
120
+ test('throws after being destroyed', config('sync'), () => {
121
+ const stream = new fs.Utf8Stream({ dest: 'u8-destroyed.log', sync: true });
122
+ stream.destroy();
123
+
124
+ assert.throws(() => stream.write('x'), { code: 'ERR_INVALID_STATE' });
125
+ assert.throws(() => stream.flushSync(), { code: 'ERR_INVALID_STATE' });
126
+ assert.throws(() => stream.end(), { code: 'ERR_INVALID_STATE' });
127
+ stream.destroy(); // A second destroy is a no-op
128
+ });
129
+
130
+ test('validates its options', () => {
131
+ assert.throws(() => new fs.Utf8Stream(), { code: 'ERR_INVALID_ARG_TYPE' });
132
+ assert.throws(() => new fs.Utf8Stream({ dest: 'u8-invalid.log', contentMode: 'ascii' as 'utf8' }), { code: 'ERR_INVALID_ARG_VALUE' });
133
+ assert.throws(() => new fs.Utf8Stream({ dest: 'u8-invalid.log', minLength: 100, maxWrite: 50 }), { code: 'ERR_INVALID_ARG_VALUE' });
134
+ assert.throws(() => new fs.Utf8Stream({ dest: 'u8-invalid.log', sync: 'yes' as unknown as boolean }), { code: 'ERR_INVALID_ARG_TYPE' });
135
+ assert.throws(() => new fs.Utf8Stream({ dest: 'u8-invalid.log', fs: { write: 5 } }), { code: 'ERR_INVALID_ARG_TYPE' });
136
+ });
137
+
138
+ test('writes asynchronously and emits events', config('async'), async () => {
139
+ const stream = new fs.Utf8Stream({ dest: 'u8-async.log' });
140
+
141
+ const events: string[] = [];
142
+ for (const event of ['ready', 'write', 'finish', 'close'] as const) stream.on(event, () => events.push(event));
143
+
144
+ assert.equal(stream.write('async hello\n'), true);
145
+ stream.end();
146
+ await new Promise<void>(resolve => stream.on('close', () => resolve()));
147
+
148
+ assert.deepEqual(events, ['ready', 'write', 'finish', 'close']);
149
+ assert.equal(fs.readFileSync('u8-async.log', 'utf8'), 'async hello\n');
150
+ });
151
+
152
+ test('flush invokes its callback once written', config('async'), async () => {
153
+ const stream = new fs.Utf8Stream({ dest: 'u8-flush.log', minLength: 4096 });
154
+ await new Promise<void>(resolve => stream.on('ready', () => resolve()));
155
+
156
+ stream.write('flushed');
157
+ assert.equal(fs.readFileSync('u8-flush.log', 'utf8'), '');
158
+
159
+ await new Promise<void>((resolve, reject) => stream.flush(err => (err ? reject(err) : resolve())));
160
+ assert.equal(fs.readFileSync('u8-flush.log', 'utf8'), 'flushed');
161
+
162
+ stream.destroy();
163
+ });
164
+ });
@@ -87,4 +87,27 @@ suite('Concurrency', config('write', 'async'), () => {
87
87
  await writer.close();
88
88
  assert.equal(await fs.promises.readFile('/issue-303-close.txt', 'utf8'), 'DIRTYnal');
89
89
  });
90
+
91
+ test('concurrent recursive mkdir of overlapping trees does not fail #308', async () => {
92
+ const paths = ['/308/a', '/308/a', '/308/a/b', '/308/a/b', '/308/c', '/308/c', '/308-2', '/308-2'];
93
+
94
+ const results = await Promise.allSettled(paths.map(path => fs.promises.mkdir(path, { recursive: true })));
95
+
96
+ const rejected = results.filter(r => r.status == 'rejected');
97
+ assert.deepEqual(
98
+ rejected.map(r => r.reason.code ?? r.reason.message),
99
+ []
100
+ );
101
+
102
+ for (const path of paths) assert((await fs.promises.stat(path)).isDirectory());
103
+ });
104
+
105
+ test('parallel recursive mkdir of the same path creates it once #308', async () => {
106
+ const results = await Promise.all(Array.from({ length: 8 }, () => fs.promises.mkdir('/308-same/nested', { recursive: true })));
107
+
108
+ assert.deepEqual(
109
+ results.filter(result => result !== undefined),
110
+ ['/308-same']
111
+ );
112
+ });
90
113
  });
@@ -45,6 +45,13 @@ suite('Directories', config('directories'), () => {
45
45
  assert.equal((await fs.promises.stat('/recursiveP/A/B/C/D')).mode, fs.constants.S_IFDIR | 0o777);
46
46
  });
47
47
 
48
+ test('mkdir, recursive with a file in the way', config('async'), async () => {
49
+ await fs.promises.writeFile('/recursiveF', 'Sample content');
50
+
51
+ await assert.rejects(fs.promises.mkdir('/recursiveF', { recursive: true }), { code: 'EEXIST', path: '/recursiveF' });
52
+ await assert.rejects(fs.promises.mkdir('/recursiveF/A/B', { recursive: true }), { code: 'ENOTDIR', path: '/recursiveF/A/B' });
53
+ });
54
+
48
55
  test('mkdirSync, recursive', config('sync'), () => {
49
56
  assert.equal(fs.mkdirSync('/recursiveS/A/B', { recursive: true, mode: 0o755 }), '/recursiveS');
50
57
  assert.equal(fs.mkdirSync('/recursiveS/A/B/C/D', { recursive: true, mode: 0o777 }), '/recursiveS/A/B/C');
@@ -57,6 +64,13 @@ suite('Directories', config('directories'), () => {
57
64
  assert.equal(fs.statSync('/recursiveS/A/B/C/D').mode, fs.constants.S_IFDIR | 0o777);
58
65
  });
59
66
 
67
+ test('mkdirSync, recursive with a file in the way', config('sync'), () => {
68
+ fs.writeFileSync('/recursiveSF', 'Sample content');
69
+
70
+ assert.throws(() => fs.mkdirSync('/recursiveSF', { recursive: true }), { code: 'EEXIST', path: '/recursiveSF' });
71
+ assert.throws(() => fs.mkdirSync('/recursiveSF/A/B', { recursive: true }), { code: 'ENOTDIR', path: '/recursiveSF/A/B' });
72
+ });
73
+
60
74
  test('rmdir (non-empty)', config('async'), async () => {
61
75
  await fs.promises.mkdir('/rmdirTest');
62
76
  await fs.promises.mkdir('/rmdirTest/rmdirTest2');
@@ -165,4 +179,11 @@ suite('Directories', config('directories'), () => {
165
179
  fs.writeFileSync('/мой-файл.txt', 'HELLO!', 'utf-8');
166
180
  assert(fs.readdirSync('/').includes('мой-файл.txt'));
167
181
  });
182
+
183
+ test('re-creating a deleted path still shows up in readdir #305', config('sync'), () => {
184
+ fs.unlinkSync('/elipses.txt');
185
+ fs.writeFileSync('/elipses.txt', '...');
186
+
187
+ assert(fs.readdirSync('/').includes('elipses.txt'));
188
+ });
168
189
  });
@@ -83,4 +83,150 @@ suite('Links', config('symlinks'), () => {
83
83
  const linkContent = await fs.promises.readFile(link, 'utf8');
84
84
  assert.equal(targetContent, linkContent);
85
85
  });
86
+
87
+ test('mkdir does not follow a symlink at the final component', config('async'), async () => {
88
+ await fs.promises.mkdir('/mkdir-real');
89
+ await fs.promises.symlink('/mkdir-real', '/mkdir-link');
90
+ await fs.promises.writeFile('/mkdir-file', 'contents');
91
+ await fs.promises.symlink('/mkdir-file', '/mkdir-flink');
92
+ await fs.promises.symlink('/mkdir-missing', '/mkdir-dangling');
93
+
94
+ await assert.rejects(fs.promises.mkdir('/mkdir-link'), { code: 'EEXIST' });
95
+ await assert.rejects(fs.promises.mkdir('/mkdir-dangling'), { code: 'EEXIST' });
96
+ await assert.rejects(fs.promises.mkdir('/mkdir-flink', { recursive: true }), { code: 'EEXIST' });
97
+ await assert.rejects(fs.promises.mkdir('/mkdir-dangling', { recursive: true }), { code: 'ENOENT' });
98
+
99
+ // A link pointing at a directory satisfies a recursive mkdir and is left as a link
100
+ assert.equal(await fs.promises.mkdir('/mkdir-link', { recursive: true }), undefined);
101
+ assert((await fs.promises.lstat('/mkdir-link')).isSymbolicLink());
102
+ assert(!(await fs.promises.exists('/mkdir-missing')));
103
+ });
104
+
105
+ test('mkdir follows symlinks for intermediate components', config('async'), async () => {
106
+ assert.equal(await fs.promises.mkdir('/mkdir-link/direct'), undefined);
107
+ assert.equal(await fs.promises.mkdir('/mkdir-link/nested/deep', { recursive: true }), '/mkdir-link/nested');
108
+
109
+ assert.deepEqual((await fs.promises.readdir('/mkdir-real')).sort(), ['direct', 'nested']);
110
+ assert((await fs.promises.stat('/mkdir-real/nested/deep')).isDirectory());
111
+
112
+ await assert.rejects(fs.promises.mkdir('/mkdir-flink/x', { recursive: true }), { code: 'ENOTDIR' });
113
+
114
+ // Node reports the link itself here, unlike `mkdirSync` which reports the requested path with ENOENT
115
+ await assert.rejects(fs.promises.mkdir('/mkdir-dangling/x', { recursive: true }), { code: 'ENOTDIR', path: '/mkdir-dangling' });
116
+ assert(!(await fs.promises.exists('/mkdir-missing')));
117
+ });
118
+
119
+ test('mkdirSync does not follow a symlink at the final component', config('sync'), () => {
120
+ fs.mkdirSync('/mkdirS-real');
121
+ fs.symlinkSync('/mkdirS-real', '/mkdirS-link');
122
+ fs.writeFileSync('/mkdirS-file', 'contents');
123
+ fs.symlinkSync('/mkdirS-file', '/mkdirS-flink');
124
+ fs.symlinkSync('/mkdirS-missing', '/mkdirS-dangling');
125
+
126
+ assert.throws(() => fs.mkdirSync('/mkdirS-link'), { code: 'EEXIST' });
127
+ assert.throws(() => fs.mkdirSync('/mkdirS-dangling'), { code: 'EEXIST' });
128
+ assert.throws(() => fs.mkdirSync('/mkdirS-flink', { recursive: true }), { code: 'EEXIST' });
129
+ assert.throws(() => fs.mkdirSync('/mkdirS-dangling', { recursive: true }), { code: 'ENOENT' });
130
+
131
+ assert.equal(fs.mkdirSync('/mkdirS-link', { recursive: true }), undefined);
132
+ assert(fs.lstatSync('/mkdirS-link').isSymbolicLink());
133
+ assert(!fs.existsSync('/mkdirS-missing'));
134
+ });
135
+
136
+ test('mkdirSync follows symlinks for intermediate components', config('sync'), () => {
137
+ assert.equal(fs.mkdirSync('/mkdirS-link/direct'), undefined);
138
+ assert.equal(fs.mkdirSync('/mkdirS-link/nested/deep', { recursive: true }), '/mkdirS-link/nested');
139
+
140
+ assert.deepEqual(fs.readdirSync('/mkdirS-real').sort(), ['direct', 'nested']);
141
+ assert(fs.statSync('/mkdirS-real/nested/deep').isDirectory());
142
+
143
+ assert.throws(() => fs.mkdirSync('/mkdirS-flink/x', { recursive: true }), { code: 'ENOTDIR' });
144
+ assert.throws(() => fs.mkdirSync('/mkdirS-dangling/x', { recursive: true }), { code: 'ENOENT', path: '/mkdirS-dangling/x' });
145
+ assert(!fs.existsSync('/mkdirS-missing'));
146
+ });
147
+
148
+ test('writeFile and appendFile follow symlinks', config('async'), async () => {
149
+ await fs.promises.mkdir('/wf-real');
150
+ await fs.promises.symlink('/wf-real', '/wf-link');
151
+ await fs.promises.writeFile('/wf-real/f.txt', 'original');
152
+ await fs.promises.symlink('/wf-real/f.txt', '/wf-flink');
153
+ await fs.promises.symlink('/wf-missing.txt', '/wf-dangling');
154
+
155
+ await fs.promises.writeFile('/wf-link/new.txt', 'hi');
156
+ assert.equal(await fs.promises.readFile('/wf-real/new.txt', 'utf8'), 'hi');
157
+
158
+ // A link to a file is written through, not replaced
159
+ await fs.promises.writeFile('/wf-flink', 'replaced');
160
+ await fs.promises.appendFile('/wf-flink', '+');
161
+ assert.equal(await fs.promises.readFile('/wf-real/f.txt', 'utf8'), 'replaced+');
162
+ assert((await fs.promises.lstat('/wf-flink')).isSymbolicLink());
163
+
164
+ // Writing through a dangling link creates its target
165
+ await fs.promises.writeFile('/wf-dangling', 'made');
166
+ assert.equal(await fs.promises.readFile('/wf-missing.txt', 'utf8'), 'made');
167
+ assert((await fs.promises.lstat('/wf-dangling')).isSymbolicLink());
168
+ });
169
+
170
+ test('writeFileSync and appendFileSync follow symlinks', config('sync'), () => {
171
+ fs.mkdirSync('/wfS-real');
172
+ fs.symlinkSync('/wfS-real', '/wfS-link');
173
+ fs.writeFileSync('/wfS-real/f.txt', 'original');
174
+ fs.symlinkSync('/wfS-real/f.txt', '/wfS-flink');
175
+ fs.symlinkSync('/wfS-missing.txt', '/wfS-dangling');
176
+
177
+ fs.writeFileSync('/wfS-link/new.txt', 'hi');
178
+ assert.equal(fs.readFileSync('/wfS-real/new.txt', 'utf8'), 'hi');
179
+
180
+ fs.writeFileSync('/wfS-flink', 'replaced');
181
+ fs.appendFileSync('/wfS-flink', '+');
182
+ assert.equal(fs.readFileSync('/wfS-real/f.txt', 'utf8'), 'replaced+');
183
+ assert(fs.lstatSync('/wfS-flink').isSymbolicLink());
184
+
185
+ fs.writeFileSync('/wfS-dangling', 'made');
186
+ assert.equal(fs.readFileSync('/wfS-missing.txt', 'utf8'), 'made');
187
+ assert(fs.lstatSync('/wfS-dangling').isSymbolicLink());
188
+ });
189
+
190
+ test('cp preserves a symlink by default #304', async () => {
191
+ await fs.promises.mkdir('/cp-src');
192
+ await fs.promises.writeFile('/cp-src/real.txt', 'contents');
193
+ await fs.promises.symlink('real.txt', '/cp-src/link.txt');
194
+
195
+ await fs.promises.cp('/cp-src', '/cp-dst', { recursive: true });
196
+
197
+ const stats = await fs.promises.lstat('/cp-dst/link.txt');
198
+ assert(stats.isSymbolicLink());
199
+
200
+ // A relative target is resolved against the link's directory, so the copy points at the original file
201
+ assert.equal(await fs.promises.readlink('/cp-dst/link.txt'), '/cp-src/real.txt');
202
+ });
203
+
204
+ test('cp with verbatimSymlinks keeps the target unresolved', async () => {
205
+ await fs.promises.cp('/cp-src', '/cp-verbatim', { recursive: true, verbatimSymlinks: true });
206
+
207
+ assert((await fs.promises.lstat('/cp-verbatim/link.txt')).isSymbolicLink());
208
+ assert.equal(await fs.promises.readlink('/cp-verbatim/link.txt'), 'real.txt');
209
+ });
210
+
211
+ test('cp with dereference copies the symlink target contents #304', async () => {
212
+ await fs.promises.cp('/cp-src', '/cp-deref', { recursive: true, dereference: true });
213
+
214
+ const stats = await fs.promises.lstat('/cp-deref/link.txt');
215
+ assert(!stats.isSymbolicLink());
216
+ assert.equal(await fs.promises.readFile('/cp-deref/link.txt', 'utf8'), 'contents');
217
+ });
218
+
219
+ test('cpSync preserves a symlink #304', () => {
220
+ fs.writeFileSync('/cp-sync-target.txt', 'contents');
221
+ fs.symlinkSync('cp-sync-target.txt', '/cp-sync-link');
222
+
223
+ fs.cpSync('/cp-sync-link', '/cp-sync-link-copy');
224
+
225
+ const stats = fs.lstatSync('/cp-sync-link-copy');
226
+ assert(stats.isSymbolicLink());
227
+ assert.equal(fs.readlinkSync('/cp-sync-link-copy'), '/cp-sync-target.txt');
228
+
229
+ fs.cpSync('/cp-sync-link', '/cp-sync-link-verbatim', { verbatimSymlinks: true });
230
+ assert.equal(fs.readlinkSync('/cp-sync-link-verbatim'), 'cp-sync-target.txt');
231
+ });
86
232
  });
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
- import { defaultContext, hasAccess } from '@zenfs/core';
2
+ import { defaultContext, hasAccess, type Stats } from '@zenfs/core';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
5
  import { config, fs } from '../common.js';
@@ -72,6 +72,35 @@ suite('Stats', () => {
72
72
  Object.assign(defaultContext.credentials, prevCredentials);
73
73
  });
74
74
 
75
+ const missing = '/does-not-exist';
76
+
77
+ test('statSync throws for a missing entry', config('sync'), () => {
78
+ assert.throws(() => fs.statSync(missing), { code: 'ENOENT' });
79
+ assert.throws(() => fs.statSync(missing, { throwIfNoEntry: true }), { code: 'ENOENT' });
80
+ });
81
+
82
+ test('statSync with throwIfNoEntry: false', config('sync'), () => {
83
+ assert.equal(fs.statSync(missing, { throwIfNoEntry: false }), undefined);
84
+ assert.equal(fs.statSync(existing_file + '/nope', { throwIfNoEntry: false }), undefined);
85
+ });
86
+
87
+ test('lstatSync with throwIfNoEntry: false', config('sync'), () => {
88
+ assert.throws(() => fs.lstatSync(missing), { code: 'ENOENT' });
89
+ assert.equal(fs.lstatSync(missing, { throwIfNoEntry: false }), undefined);
90
+ });
91
+
92
+ test('stat with throwIfNoEntry: false', config('async'), async () => {
93
+ await assert.rejects(fs.promises.stat(missing), { code: 'ENOENT' });
94
+ assert.equal(await fs.promises.stat(missing, { throwIfNoEntry: false }), undefined);
95
+ assert.equal(await fs.promises.stat(existing_file + '/nope', { throwIfNoEntry: false }), undefined);
96
+ });
97
+
98
+ test('stat callback with throwIfNoEntry: false', config('async'), async () => {
99
+ const { promise, resolve, reject } = Promise.withResolvers<Stats | undefined>();
100
+ fs.stat(missing, { throwIfNoEntry: false }, (error, stats) => (error ? reject(error) : resolve(stats)));
101
+ assert.equal(await promise, undefined);
102
+ });
103
+
75
104
  test('stat file', config('async'), async () => {
76
105
  const stats = await fs.promises.stat(existing_file);
77
106
  assert(!stats.isDirectory());
@@ -47,6 +47,7 @@ function toNative(path: PathLike): string {
47
47
 
48
48
  /** Translate a native path back into a test path */
49
49
  function fromNative(path: string): string {
50
+ if (!isAbsolute(path)) return path;
50
51
  return resolve('/', relative(root, path));
51
52
  }
52
53