Skip to content

Commit 6cd90e1

Browse files
committed
test: add VFS internals coverage tests
Add tests for internal VFS utility functions and provider base class to improve code coverage: - router.js: splitPath, getParentPath, getBaseName - provider.js: readonly EROFS checks for all write operations - provider.js: ERR_METHOD_NOT_IMPLEMENTED for unimplemented methods PR-URL: #61478
1 parent 306b825 commit 6cd90e1

File tree

1 file changed

+318
-0
lines changed

1 file changed

+318
-0
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
7+
// Test internal VFS utility functions for coverage.
8+
9+
// === Router utility functions ===
10+
const {
11+
splitPath,
12+
getParentPath,
13+
getBaseName,
14+
isUnderMountPoint,
15+
getRelativePath,
16+
isAbsolutePath,
17+
} = require('internal/vfs/router');
18+
19+
// splitPath
20+
assert.deepStrictEqual(splitPath('/'), []);
21+
assert.deepStrictEqual(splitPath('/foo'), ['foo']);
22+
assert.deepStrictEqual(splitPath('/foo/bar'), ['foo', 'bar']);
23+
assert.deepStrictEqual(splitPath('/a/b/c/d'), ['a', 'b', 'c', 'd']);
24+
25+
// getParentPath
26+
assert.strictEqual(getParentPath('/'), null);
27+
assert.strictEqual(getParentPath('/foo'), '/');
28+
assert.strictEqual(getParentPath('/foo/bar'), '/foo');
29+
assert.strictEqual(getParentPath('/a/b/c'), '/a/b');
30+
31+
// getBaseName
32+
assert.strictEqual(getBaseName('/foo'), 'foo');
33+
assert.strictEqual(getBaseName('/foo/bar'), 'bar');
34+
assert.strictEqual(getBaseName('/a/b/c.txt'), 'c.txt');
35+
36+
// isAbsolutePath
37+
assert.strictEqual(isAbsolutePath('/foo'), true);
38+
assert.strictEqual(isAbsolutePath('foo'), false);
39+
assert.strictEqual(isAbsolutePath('./foo'), false);
40+
41+
// isUnderMountPoint (already tested indirectly but exercised here directly)
42+
assert.strictEqual(isUnderMountPoint('/mount', '/mount'), true);
43+
assert.strictEqual(isUnderMountPoint('/mount/file', '/mount'), true);
44+
assert.strictEqual(isUnderMountPoint('/mountx', '/mount'), false);
45+
assert.strictEqual(isUnderMountPoint('/other', '/mount'), false);
46+
// Root mount point
47+
assert.strictEqual(isUnderMountPoint('/anything', '/'), true);
48+
49+
// getRelativePath
50+
assert.strictEqual(getRelativePath('/mount', '/mount'), '/');
51+
assert.strictEqual(getRelativePath('/mount/file.js', '/mount'), '/file.js');
52+
assert.strictEqual(getRelativePath('/mount/a/b', '/mount'), '/a/b');
53+
// Root mount point
54+
assert.strictEqual(getRelativePath('/foo/bar', '/'), '/foo/bar');
55+
56+
// === Provider base class readonly checks ===
57+
const { VirtualProvider } = require('internal/vfs/provider');
58+
59+
class ReadonlyProvider extends VirtualProvider {
60+
get readonly() { return true; }
61+
}
62+
63+
const readonlyProvider = new ReadonlyProvider();
64+
65+
// All write operations should throw EROFS when readonly
66+
assert.throws(() => readonlyProvider.mkdirSync('/dir'), { code: 'EROFS' });
67+
assert.throws(() => readonlyProvider.rmdirSync('/dir'), { code: 'EROFS' });
68+
assert.throws(() => readonlyProvider.unlinkSync('/file'), { code: 'EROFS' });
69+
assert.throws(() => readonlyProvider.renameSync('/a', '/b'), { code: 'EROFS' });
70+
assert.throws(() => readonlyProvider.writeFileSync('/f', 'data'), { code: 'EROFS' });
71+
assert.throws(() => readonlyProvider.appendFileSync('/f', 'data'), { code: 'EROFS' });
72+
assert.throws(() => readonlyProvider.copyFileSync('/a', '/b'), { code: 'EROFS' });
73+
assert.throws(() => readonlyProvider.symlinkSync('/target', '/link'), { code: 'EROFS' });
74+
75+
// Async versions
76+
assert.rejects(readonlyProvider.mkdir('/dir'), { code: 'EROFS' }).then(common.mustCall());
77+
assert.rejects(readonlyProvider.rmdir('/dir'), { code: 'EROFS' }).then(common.mustCall());
78+
assert.rejects(readonlyProvider.unlink('/file'), { code: 'EROFS' }).then(common.mustCall());
79+
assert.rejects(readonlyProvider.rename('/a', '/b'), { code: 'EROFS' }).then(common.mustCall());
80+
assert.rejects(readonlyProvider.writeFile('/f', 'data'), { code: 'EROFS' }).then(common.mustCall());
81+
assert.rejects(readonlyProvider.appendFile('/f', 'data'), { code: 'EROFS' }).then(common.mustCall());
82+
assert.rejects(readonlyProvider.copyFile('/a', '/b'), { code: 'EROFS' }).then(common.mustCall());
83+
assert.rejects(readonlyProvider.symlink('/target', '/link'), { code: 'EROFS' }).then(common.mustCall());
84+
85+
// === Provider base class ERR_METHOD_NOT_IMPLEMENTED for non-readonly ===
86+
const baseProvider = new VirtualProvider();
87+
88+
// These should throw ERR_METHOD_NOT_IMPLEMENTED (not readonly, just unimplemented)
89+
assert.throws(() => baseProvider.mkdirSync('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
90+
assert.throws(() => baseProvider.rmdirSync('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
91+
assert.throws(() => baseProvider.unlinkSync('/file'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
92+
assert.throws(() => baseProvider.renameSync('/a', '/b'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
93+
assert.throws(() => baseProvider.symlinkSync('/t', '/l'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
94+
assert.throws(() => baseProvider.readdirSync('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
95+
assert.throws(() => baseProvider.readlinkSync('/link'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
96+
assert.throws(() => baseProvider.watch('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
97+
assert.throws(() => baseProvider.watchAsync('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
98+
assert.throws(() => baseProvider.watchFile('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
99+
assert.throws(() => baseProvider.unwatchFile('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' });
100+
101+
// Async unimplemented methods
102+
assert.rejects(baseProvider.mkdir('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
103+
assert.rejects(baseProvider.rmdir('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
104+
assert.rejects(baseProvider.unlink('/file'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
105+
assert.rejects(baseProvider.rename('/a', '/b'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
106+
assert.rejects(baseProvider.readlink('/link'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
107+
assert.rejects(baseProvider.symlink('/t', '/l'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
108+
109+
// === Provider default implementations via VFS (covers realpath, access, exists, etc.) ===
110+
const vfs = require('node:vfs');
111+
112+
{
113+
const myVfs = vfs.create();
114+
myVfs.writeFileSync('/file.txt', 'hello');
115+
myVfs.mkdirSync('/dir');
116+
myVfs.mount('/internals-test');
117+
118+
// Realpath (default provider impl returns path as-is after stat check)
119+
assert.strictEqual(myVfs.realpathSync('/internals-test/file.txt'), '/internals-test/file.txt');
120+
assert.strictEqual(myVfs.realpathSync('/internals-test/dir'), '/internals-test/dir');
121+
122+
// Realpath for non-existent path should throw
123+
assert.throws(() => myVfs.realpathSync('/internals-test/nonexistent'), { code: 'ENOENT' });
124+
125+
// access (default provider impl just checks stat)
126+
myVfs.accessSync('/internals-test/file.txt');
127+
assert.throws(() => myVfs.accessSync('/internals-test/nonexistent'), { code: 'ENOENT' });
128+
129+
// existsSync
130+
assert.strictEqual(myVfs.existsSync('/internals-test/file.txt'), true);
131+
assert.strictEqual(myVfs.existsSync('/internals-test/nonexistent'), false);
132+
assert.strictEqual(myVfs.existsSync('/internals-test/dir'), true);
133+
134+
// internalModuleStat (0 = file, 1 = dir, -2 = not found)
135+
assert.strictEqual(myVfs.internalModuleStat('/internals-test/file.txt'), 0);
136+
assert.strictEqual(myVfs.internalModuleStat('/internals-test/dir'), 1);
137+
assert.strictEqual(myVfs.internalModuleStat('/internals-test/nope'), -2);
138+
139+
// Callback-based realpath
140+
myVfs.realpath('/internals-test/file.txt', common.mustSucceed((resolved) => {
141+
assert.ok(resolved);
142+
}));
143+
144+
// Callback-based access
145+
myVfs.access('/internals-test/file.txt', common.mustSucceed());
146+
147+
myVfs.unmount();
148+
}
149+
150+
// === VirtualFileHandle base class ERR_METHOD_NOT_IMPLEMENTED ===
151+
const { VirtualFileHandle } = require('internal/vfs/file_handle');
152+
153+
{
154+
const handle = new VirtualFileHandle('/test', 'r');
155+
156+
// Property accessors
157+
assert.strictEqual(handle.path, '/test');
158+
assert.strictEqual(handle.flags, 'r');
159+
assert.strictEqual(handle.mode, 0o644);
160+
assert.strictEqual(handle.position, 0);
161+
assert.strictEqual(handle.closed, false);
162+
163+
// Position setter
164+
handle.position = 42;
165+
assert.strictEqual(handle.position, 42);
166+
167+
// Sync methods should throw ERR_METHOD_NOT_IMPLEMENTED
168+
assert.throws(() => handle.readSync(Buffer.alloc(10), 0, 10, 0),
169+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
170+
assert.throws(() => handle.writeSync(Buffer.alloc(10), 0, 10, 0),
171+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
172+
assert.throws(() => handle.readFileSync(),
173+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
174+
assert.throws(() => handle.writeFileSync('data'),
175+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
176+
assert.throws(() => handle.statSync(),
177+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
178+
assert.throws(() => handle.truncateSync(),
179+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' });
180+
181+
// Async methods should reject with ERR_METHOD_NOT_IMPLEMENTED
182+
assert.rejects(handle.read(Buffer.alloc(10), 0, 10, 0),
183+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
184+
assert.rejects(handle.write(Buffer.alloc(10), 0, 10, 0),
185+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
186+
assert.rejects(handle.readFile(),
187+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
188+
assert.rejects(handle.writeFile('data'),
189+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
190+
assert.rejects(handle.stat(),
191+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
192+
assert.rejects(handle.truncate(),
193+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall());
194+
195+
// Close
196+
handle.closeSync();
197+
assert.strictEqual(handle.closed, true);
198+
199+
// Methods on closed handle should throw EBADF
200+
assert.throws(() => handle.readSync(Buffer.alloc(10), 0, 10, 0), { code: 'EBADF' });
201+
assert.throws(() => handle.writeSync(Buffer.alloc(10), 0, 10, 0), { code: 'EBADF' });
202+
assert.throws(() => handle.readFileSync(), { code: 'EBADF' });
203+
assert.throws(() => handle.writeFileSync('data'), { code: 'EBADF' });
204+
assert.throws(() => handle.statSync(), { code: 'EBADF' });
205+
assert.throws(() => handle.truncateSync(), { code: 'EBADF' });
206+
}
207+
208+
// === MemoryFileHandle via VFS fd operations ===
209+
{
210+
const myVfs = vfs.create();
211+
myVfs.writeFileSync('/fdtest.txt', 'abcdefghij');
212+
myVfs.mount('/fd-test');
213+
214+
// Open and read with auto-advancing position
215+
const fd = myVfs.openSync('/fd-test/fdtest.txt');
216+
const buf = Buffer.alloc(5);
217+
let bytesRead = myVfs.readSync(fd, buf, 0, 5, null);
218+
assert.strictEqual(bytesRead, 5);
219+
assert.strictEqual(buf.toString(), 'abcde');
220+
221+
// Read more - position should have advanced
222+
const buf2 = Buffer.alloc(5);
223+
bytesRead = myVfs.readSync(fd, buf2, 0, 5, null);
224+
assert.strictEqual(bytesRead, 5);
225+
assert.strictEqual(buf2.toString(), 'fghij');
226+
227+
// Read past end - should return 0
228+
const buf3 = Buffer.alloc(5);
229+
bytesRead = myVfs.readSync(fd, buf3, 0, 5, null);
230+
assert.strictEqual(bytesRead, 0);
231+
232+
// Read with explicit position (doesn't advance internal position)
233+
const buf4 = Buffer.alloc(3);
234+
bytesRead = myVfs.readSync(fd, buf4, 0, 3, 2);
235+
assert.strictEqual(bytesRead, 3);
236+
assert.strictEqual(buf4.toString(), 'cde');
237+
238+
// fstatSync
239+
const stats = myVfs.fstatSync(fd);
240+
assert.strictEqual(stats.size, 10);
241+
assert.strictEqual(stats.isFile(), true);
242+
243+
// Close
244+
myVfs.closeSync(fd);
245+
246+
// EBADF after close
247+
assert.throws(() => myVfs.readSync(fd, Buffer.alloc(5), 0, 5, 0), { code: 'EBADF' });
248+
assert.throws(() => myVfs.closeSync(fd), { code: 'EBADF' });
249+
assert.throws(() => myVfs.fstatSync(fd), { code: 'EBADF' });
250+
251+
myVfs.unmount();
252+
}
253+
254+
// === VirtualReadStream ===
255+
{
256+
const myVfs = vfs.create();
257+
myVfs.writeFileSync('/stream.txt', 'hello world');
258+
myVfs.mount('/stream-test');
259+
260+
// Basic stream read
261+
const chunks = [];
262+
const stream = myVfs.createReadStream('/stream-test/stream.txt');
263+
264+
stream.on('open', common.mustCall((fd) => {
265+
assert.strictEqual(typeof fd, 'number');
266+
}));
267+
268+
stream.on('ready', common.mustCall());
269+
270+
stream.on('data', (chunk) => {
271+
chunks.push(chunk);
272+
});
273+
274+
stream.on('end', common.mustCall(() => {
275+
const result = Buffer.concat(chunks).toString();
276+
assert.strictEqual(result, 'hello world');
277+
}));
278+
279+
stream.on('close', common.mustCall(() => {
280+
myVfs.unmount();
281+
}));
282+
}
283+
284+
// === VirtualReadStream with start/end options ===
285+
{
286+
const myVfs = vfs.create();
287+
myVfs.writeFileSync('/range.txt', '0123456789');
288+
myVfs.mount('/stream-range');
289+
290+
const chunks = [];
291+
// Read bytes 2-5 (inclusive), should get "2345"
292+
const stream = myVfs.createReadStream('/stream-range/range.txt', {
293+
start: 2,
294+
end: 5,
295+
});
296+
297+
stream.on('data', (chunk) => {
298+
chunks.push(chunk);
299+
});
300+
301+
stream.on('end', common.mustCall(() => {
302+
const result = Buffer.concat(chunks).toString();
303+
assert.strictEqual(result, '2345');
304+
myVfs.unmount();
305+
}));
306+
}
307+
308+
// === VirtualReadStream path property ===
309+
{
310+
const myVfs = vfs.create();
311+
myVfs.writeFileSync('/prop.txt', 'x');
312+
myVfs.mount('/stream-prop');
313+
314+
const stream = myVfs.createReadStream('/stream-prop/prop.txt');
315+
assert.strictEqual(stream.path, '/stream-prop/prop.txt');
316+
stream.destroy();
317+
myVfs.unmount();
318+
}

0 commit comments

Comments
 (0)