@zenfs/core 2.7.7 → 2.8.0
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.
- package/README.md +5 -0
- package/dist/backends/index.d.ts +3 -1
- package/dist/backends/index.js +3 -1
- package/dist/backends/single_buffer/backend.d.ts +42 -0
- package/dist/backends/single_buffer/backend.js +204 -0
- package/dist/backends/{single_buffer.d.ts → single_buffer/v1.d.ts} +24 -34
- package/dist/backends/{single_buffer.js → single_buffer/v1.js} +10 -26
- package/dist/backends/single_buffer/v2.d.ts +257 -0
- package/dist/backends/single_buffer/v2.js +1283 -0
- package/dist/backends/store/fs.d.ts +1 -1
- package/dist/backends/store/fs.js +34 -15
- package/dist/backends/store/map.d.ts +6 -6
- package/dist/backends/store/map.js +6 -6
- package/dist/backends/store/store.d.ts +18 -14
- package/dist/backends/store/store.js +11 -11
- package/dist/internal/contexts.d.ts +5 -0
- package/dist/internal/contexts.js +7 -1
- package/dist/mixins/async.js +26 -7
- package/dist/node/promises.js +6 -103
- package/dist/node/sync.js +6 -33
- package/dist/vfs/async.d.ts +4 -1
- package/dist/vfs/async.js +72 -5
- package/dist/vfs/sync.d.ts +4 -1
- package/dist/vfs/sync.js +65 -4
- package/package.json +2 -2
- package/tests/backend/single-buffer-rotate.worker.js +1 -1
- package/tests/backend/single-buffer-v2.test.ts +342 -0
- package/tests/backend/single-buffer-v2.worker.js +51 -0
- package/tests/backend/single-buffer.test.ts +6 -6
- package/tests/backend/single-buffer.worker.js +1 -0
- package/tests/common/async.test.ts +11 -0
- package/tests/common/context.test.ts +30 -0
- package/tests/fs/permissions.test.ts +27 -6
- package/tests/fs/watch.test.ts +14 -0
- package/tests/perf/config.json +31 -1
- package/tests/perf/single-buffer-random.ts +38 -0
- package/tests/perf/single-buffer-read.ts +28 -0
- package/tests/perf/single-buffer-write.ts +41 -0
- package/tests/setup/single-buffer-v2.ts +10 -0
- package/tests/setup/single-buffer.ts +1 -0
|
@@ -5,7 +5,7 @@ import { suite, test } from 'node:test';
|
|
|
5
5
|
import { Worker } from 'worker_threads';
|
|
6
6
|
import { sizeof } from 'memium';
|
|
7
7
|
import { fs, mount, resolveMountConfig, SingleBuffer, vfs } from '@zenfs/core';
|
|
8
|
-
import { MetadataBlock, SuperBlock } from '@zenfs/core/backends/single_buffer';
|
|
8
|
+
import { MetadataBlock, SuperBlock } from '@zenfs/core/backends/single_buffer/v1';
|
|
9
9
|
import { setupLogs } from '../logs.js';
|
|
10
10
|
|
|
11
11
|
setupLogs();
|
|
@@ -14,13 +14,13 @@ await suite('SingleBuffer', () => {
|
|
|
14
14
|
test('filesystem restoration from original buffer (with same metadata)', async () => {
|
|
15
15
|
const buffer = new ArrayBuffer(0x100000);
|
|
16
16
|
|
|
17
|
-
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer });
|
|
17
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, v2: false });
|
|
18
18
|
mount('/mnt', writable);
|
|
19
19
|
|
|
20
20
|
fs.writeFileSync('/mnt/example.ts', 'console.log("hello world")', 'utf-8');
|
|
21
21
|
const stats = fs.statSync('/mnt/example.ts');
|
|
22
22
|
|
|
23
|
-
const snapshot = await resolveMountConfig({ backend: SingleBuffer, buffer });
|
|
23
|
+
const snapshot = await resolveMountConfig({ backend: SingleBuffer, buffer, v2: false });
|
|
24
24
|
mount('/snapshot', snapshot);
|
|
25
25
|
|
|
26
26
|
const snapshotStats = fs.statSync('/snapshot/example.ts');
|
|
@@ -31,7 +31,7 @@ await suite('SingleBuffer', () => {
|
|
|
31
31
|
test('cross-thread SharedArrayBuffer', async () => {
|
|
32
32
|
const sharedBuffer = new SharedArrayBuffer(0x100000);
|
|
33
33
|
|
|
34
|
-
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer: sharedBuffer });
|
|
34
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer: sharedBuffer, v2: false });
|
|
35
35
|
fs.mkdirSync('/shared');
|
|
36
36
|
mount('/shared', writable);
|
|
37
37
|
|
|
@@ -141,7 +141,7 @@ await suite('SingleBuffer', () => {
|
|
|
141
141
|
const mountPoint = '/sbfs-reliability';
|
|
142
142
|
const verifyMountPoint = '/sbfs-verify';
|
|
143
143
|
const buffer = new ArrayBuffer(0x400000);
|
|
144
|
-
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'reliability' });
|
|
144
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'reliability', v2: false });
|
|
145
145
|
mount(mountPoint, writable);
|
|
146
146
|
|
|
147
147
|
const filePath = `${mountPoint}/payload.bin`;
|
|
@@ -186,7 +186,7 @@ await suite('SingleBuffer', () => {
|
|
|
186
186
|
test('keeps metadata aligned when files have uneven sizes #309', async () => {
|
|
187
187
|
const mountPoint = '/sbfs-rotation';
|
|
188
188
|
const buffer = new ArrayBuffer(0x400000);
|
|
189
|
-
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'rotation' });
|
|
189
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'rotation', v2: false });
|
|
190
190
|
mount(mountPoint, writable);
|
|
191
191
|
|
|
192
192
|
// Uneven writes leave used_bytes between alignment boundaries when the metadata block fills up.
|
|
@@ -32,4 +32,15 @@ suite('Async()', () => {
|
|
|
32
32
|
assert.deepEqual(calls, ['mkdirSync', 'rmdirSync', 'createFileSync', 'unlinkSync']);
|
|
33
33
|
assert.deepEqual(await fs.readdir('/'), []);
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
test('async operations see queued sync operations', async () => {
|
|
37
|
+
const fs = new AsyncMemory(new InMemoryStore());
|
|
38
|
+
fs.checkRootSync();
|
|
39
|
+
await fs.ready();
|
|
40
|
+
|
|
41
|
+
fs.mkdirSync('/dir', { mode: 0o755, uid: 0, gid: 0 });
|
|
42
|
+
assert.equal((await fs.stat('/dir')).mode & 0o777, 0o755);
|
|
43
|
+
fs.rmdirSync('/dir');
|
|
44
|
+
assert.equal(await fs.exists('/dir'), false);
|
|
45
|
+
});
|
|
35
46
|
});
|
|
@@ -134,4 +134,34 @@ suite('Context', () => {
|
|
|
134
134
|
|
|
135
135
|
assert.doesNotThrow(() => ctx.bind({ credentials: { uid: 500, gid: 500 } }));
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
test('umask is inherited by children', () => {
|
|
139
|
+
const parent = bindContext({ umask: 0o027 });
|
|
140
|
+
assert.equal(parent.bind({}).umask, 0o027);
|
|
141
|
+
assert.equal(parent.bind({ umask: 0o077 }).umask, 0o077);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('umask applies to created files and directories', async () => {
|
|
145
|
+
fs.mkdirSync('/masked');
|
|
146
|
+
const masked = bindContext({ root: '/masked', umask: 0o027 });
|
|
147
|
+
|
|
148
|
+
masked.fs.writeFileSync('/sync.txt', '', { mode: 0o666 });
|
|
149
|
+
assert.equal(masked.fs.statSync('/sync.txt').mode & 0o7777, 0o640);
|
|
150
|
+
|
|
151
|
+
await masked.fs.promises.writeFile('/async.txt', '', { mode: 0o666 });
|
|
152
|
+
assert.equal(masked.fs.statSync('/async.txt').mode & 0o7777, 0o640);
|
|
153
|
+
|
|
154
|
+
masked.fs.mkdirSync('/a/b', { recursive: true, mode: 0o777 });
|
|
155
|
+
assert.equal(masked.fs.statSync('/a').mode & 0o7777, 0o750);
|
|
156
|
+
assert.equal(masked.fs.statSync('/a/b').mode & 0o7777, 0o750);
|
|
157
|
+
|
|
158
|
+
await masked.fs.promises.mkdir('/c', 0o1777);
|
|
159
|
+
assert.equal(masked.fs.statSync('/c').mode & 0o7777, 0o1750);
|
|
160
|
+
|
|
161
|
+
masked.fs.symlinkSync('/sync.txt', '/link');
|
|
162
|
+
assert.equal(masked.fs.lstatSync('/link').mode & 0o7777, 0o777);
|
|
163
|
+
|
|
164
|
+
masked.fs.chmodSync('/sync.txt', 0o666);
|
|
165
|
+
assert.equal(masked.fs.statSync('/sync.txt').mode & 0o7777, 0o666);
|
|
166
|
+
});
|
|
137
167
|
});
|
|
@@ -133,11 +133,11 @@ suite('Permissions', config('permissions'), () => {
|
|
|
133
133
|
await rootFS.promises.writeFile('/removal-root/file', 'x', { mode: 0o666 });
|
|
134
134
|
await rootFS.promises.mkdir('/removal-root/dir', { mode: 0o777 });
|
|
135
135
|
await rootFS.promises.mkdir('/removal-own', { mode: 0o755 });
|
|
136
|
-
await rootFS.promises.
|
|
136
|
+
await rootFS.promises.chown('/removal-own', 1000, 1000);
|
|
137
137
|
await rootFS.promises.writeFile('/removal-own/readonly', 'x', { mode: 0o444 });
|
|
138
138
|
await rootFS.promises.mkdir('/removal-own/readonly-dir', { mode: 0o555 });
|
|
139
139
|
await rootFS.promises.mkdir('/removal-unreadable', { mode: 0o300 });
|
|
140
|
-
await rootFS.promises.
|
|
140
|
+
await rootFS.promises.chown('/removal-unreadable', 1000, 1000);
|
|
141
141
|
await rootFS.promises.writeFile('/removal-unreadable/file', 'x');
|
|
142
142
|
|
|
143
143
|
await assert.rejects(alice.fs.promises.unlink('/removal-root/file'), { code: 'EACCES' });
|
|
@@ -177,7 +177,7 @@ suite('Permissions', config('permissions'), () => {
|
|
|
177
177
|
assert.deepEqual(rootFS.readdirSync('/sticky'), []);
|
|
178
178
|
|
|
179
179
|
await rootFS.promises.mkdir('/sticky-alice', { mode: 0o1777 });
|
|
180
|
-
await rootFS.promises.
|
|
180
|
+
await rootFS.promises.chown('/sticky-alice', 1000, 1000);
|
|
181
181
|
await bob.fs.promises.writeFile('/sticky-alice/bob', 'b');
|
|
182
182
|
await alice.fs.promises.unlink('/sticky-alice/bob');
|
|
183
183
|
});
|
|
@@ -211,6 +211,27 @@ suite('Permissions', config('permissions'), () => {
|
|
|
211
211
|
assert.equal(stats.uid, 2000);
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
test('changing metadata needs no read or write access, even on directories #327', async () => {
|
|
215
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
216
|
+
|
|
217
|
+
await rootFS.promises.writeFile('/metadata-file', 'x', { mode: 0o000 });
|
|
218
|
+
await rootFS.promises.mkdir('/metadata-dir', { mode: 0o000 });
|
|
219
|
+
|
|
220
|
+
for (const path of ['/metadata-file', '/metadata-dir']) {
|
|
221
|
+
rootFS.chownSync(path, 1000, 1000);
|
|
222
|
+
alice.fs.chmodSync(path, 0o500);
|
|
223
|
+
await alice.fs.promises.chmod(path, 0o400);
|
|
224
|
+
alice.fs.utimesSync(path, 1, 2);
|
|
225
|
+
await alice.fs.promises.utimes(path, 3, 4);
|
|
226
|
+
alice.fs.chownSync(path, -1, -1);
|
|
227
|
+
await alice.fs.promises.chown(path, -1, -1);
|
|
228
|
+
|
|
229
|
+
const stats = rootFS.statSync(path);
|
|
230
|
+
assert.equal(stats.mode & 0o777, 0o400);
|
|
231
|
+
assert.equal(stats.mtimeMs, 4000);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
214
235
|
test('moving a directory to another parent needs write permission on it', async () => {
|
|
215
236
|
const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
216
237
|
|
|
@@ -249,8 +270,8 @@ suite('Permissions', config('permissions'), () => {
|
|
|
249
270
|
alice.fs.chmodSync('/sid/file', 0o2755);
|
|
250
271
|
assert.equal(modeOf('/sid/file'), 0o755);
|
|
251
272
|
|
|
252
|
-
await alice.fs.promises.
|
|
253
|
-
await alice.fs.promises.
|
|
273
|
+
await alice.fs.promises.chmod('/sid/dir', 0o6755);
|
|
274
|
+
await alice.fs.promises.chown('/sid/dir', -1, -1);
|
|
254
275
|
assert.equal(modeOf('/sid/dir'), 0o6755);
|
|
255
276
|
});
|
|
256
277
|
|
|
@@ -271,7 +292,7 @@ suite('Permissions', config('permissions'), () => {
|
|
|
271
292
|
const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
|
|
272
293
|
|
|
273
294
|
await rootFS.promises.mkdir('/private', { mode: 0o700 });
|
|
274
|
-
await rootFS.promises.
|
|
295
|
+
await rootFS.promises.chown('/private', 1000, 1000);
|
|
275
296
|
await rootFS.promises.mkdir('/private/inner', { mode: 0o755 });
|
|
276
297
|
await rootFS.promises.writeFile('/private/inner/readable.txt', 'secret', { mode: 0o644 });
|
|
277
298
|
await rootFS.promises.symlink('/private', '/private-link');
|
package/tests/fs/watch.test.ts
CHANGED
|
@@ -41,6 +41,20 @@ suite('Watch', config('watch'), () => {
|
|
|
41
41
|
assert.equal(filename, 'test.txt');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test('Events emitted on metadata change', async () => {
|
|
45
|
+
const { promise, resolve } = Promise.withResolvers<[string, string]>();
|
|
46
|
+
|
|
47
|
+
using watcher = fs.watch(testFile, (eventType, filename) => {
|
|
48
|
+
resolve([eventType, filename]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
fs.chmodSync(testFile, 0o600);
|
|
52
|
+
|
|
53
|
+
const [eventType, filename] = await withTimeout(promise);
|
|
54
|
+
assert.equal(eventType, 'change');
|
|
55
|
+
assert.equal(filename, 'test.txt');
|
|
56
|
+
});
|
|
57
|
+
|
|
44
58
|
test('Events are emitted on delete', async () => {
|
|
45
59
|
const { promise, resolve } = Promise.withResolvers<string>();
|
|
46
60
|
|
package/tests/perf/config.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"flags": {
|
|
3
|
-
"prep": ["writeFile", "stream"]
|
|
3
|
+
"prep": ["writeFile", "stream"],
|
|
4
|
+
"sbfs_v2": [true, false]
|
|
4
5
|
},
|
|
5
6
|
"tests": [
|
|
6
7
|
{
|
|
@@ -79,6 +80,35 @@
|
|
|
79
80
|
{ "name": "2000 appends", "value": { "writes": 2000 } },
|
|
80
81
|
{ "name": "8000 appends", "cpu": 1, "value": { "writes": 8000 } }
|
|
81
82
|
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"path": "single-buffer-read.ts",
|
|
86
|
+
"name": "Random 4 KB reads from one file in a SingleBuffer",
|
|
87
|
+
"flags": ["sbfs_v2"],
|
|
88
|
+
"measure": {
|
|
89
|
+
"bytes": { "throughput": "s", "unit": "MB" },
|
|
90
|
+
"reads": { "cost": true, "unit": "read" }
|
|
91
|
+
},
|
|
92
|
+
"configurations": [
|
|
93
|
+
{ "name": "1 MiB", "value": { "size": 1048576, "count": 256 } },
|
|
94
|
+
{ "name": "8 MiB", "value": { "size": 8388608, "count": 1024 } },
|
|
95
|
+
{ "name": "32 MiB", "cpu": 1, "value": { "size": 33554432, "count": 1024 } }
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"path": "single-buffer-write.ts",
|
|
100
|
+
"name": "Random 4 KB writes to one file in a SingleBuffer",
|
|
101
|
+
"flags": ["sbfs_v2"],
|
|
102
|
+
"measure": {
|
|
103
|
+
"bytes": { "throughput": "s", "unit": "MB" },
|
|
104
|
+
"writes": { "cost": true, "unit": "write" }
|
|
105
|
+
},
|
|
106
|
+
"configurations": [
|
|
107
|
+
{ "name": "1 MiB, flushed", "value": { "size": 1048576, "count": 256, "flush": 1, "sync": 0 } },
|
|
108
|
+
{ "name": "1 MiB, O_SYNC", "value": { "size": 1048576, "count": 256, "flush": 0, "sync": 1 } },
|
|
109
|
+
{ "name": "8 MiB, flushed", "cpu": 1, "value": { "size": 8388608, "count": 1024, "flush": 1, "sync": 0 } },
|
|
110
|
+
{ "name": "8 MiB, O_SYNC", "cpu": 1, "value": { "size": 8388608, "count": 1024, "flush": 0, "sync": 1 } }
|
|
111
|
+
]
|
|
82
112
|
}
|
|
83
113
|
]
|
|
84
114
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// Helpers for random access to one file in a SingleBuffer.
|
|
3
|
+
import { configureSingle, SingleBuffer } from '@zenfs/core';
|
|
4
|
+
import { fs, KB, MB } from './common.ts';
|
|
5
|
+
|
|
6
|
+
export { fs };
|
|
7
|
+
|
|
8
|
+
export const block = 4 * KB;
|
|
9
|
+
|
|
10
|
+
export interface Config {
|
|
11
|
+
/** Size of the file */
|
|
12
|
+
size: number;
|
|
13
|
+
|
|
14
|
+
/** Number of blocks accessed, each once */
|
|
15
|
+
count: number;
|
|
16
|
+
|
|
17
|
+
sbfs_v2: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Mounts a SingleBuffer with room for the file twice over, then creates the file */
|
|
21
|
+
export async function mountWithFile(config: Config): Promise<void> {
|
|
22
|
+
await configureSingle({ backend: SingleBuffer, buffer: new ArrayBuffer(config.size * 2 + 4 * MB), v2: config.sbfs_v2 });
|
|
23
|
+
fs.writeFileSync('/f', new Uint8Array(config.size).fill(0x5a));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Offsets of `count` distinct blocks of the file, in a shuffled order that is the same on every run */
|
|
27
|
+
export function offsets(config: Config): number[] {
|
|
28
|
+
const blocks = Array.from({ length: config.size / block }, (_, i) => i);
|
|
29
|
+
|
|
30
|
+
let seed = 0x2545f491;
|
|
31
|
+
for (let i = blocks.length - 1; i > 0; i--) {
|
|
32
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
33
|
+
const j = seed % (i + 1);
|
|
34
|
+
[blocks[i], blocks[j]] = [blocks[j], blocks[i]];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return blocks.slice(0, config.count).map(i => i * block);
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// 4 KB reads at random offsets in one file in a SingleBuffer.
|
|
3
|
+
// The file is opened fresh for each iteration, so every read misses the vnode cache and reaches the store.
|
|
4
|
+
import { block, fs, mountWithFile, offsets, type Config } from './single-buffer-random.ts';
|
|
5
|
+
|
|
6
|
+
interface State {
|
|
7
|
+
offsets: number[];
|
|
8
|
+
target: Uint8Array;
|
|
9
|
+
fd?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function setup(config: Config): Promise<State> {
|
|
13
|
+
await mountWithFile(config);
|
|
14
|
+
return { offsets: offsets(config), target: new Uint8Array(block) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function before(config: Config, state: State): void {
|
|
18
|
+
state.fd = fs.openSync('/f', 'r');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function test(config: Config, state: State) {
|
|
22
|
+
for (const offset of state.offsets) fs.readSync(state.fd!, state.target, 0, block, offset);
|
|
23
|
+
return { reads: state.offsets.length, bytes: state.offsets.length * block };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function after(config: Config, state: State): void {
|
|
27
|
+
fs.closeSync(state.fd!);
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// 4 KB writes at random offsets in one file in a SingleBuffer.
|
|
3
|
+
// By default, only the writes are timed, and they stay in the vnode cache until the untimed close.
|
|
4
|
+
// With `flush`, the close is timed too, which writes the cached data to the store.
|
|
5
|
+
// With `osync`, the file is opened with O_SYNC, so every write reaches the store.
|
|
6
|
+
import { block, fs, mountWithFile, offsets, type Config as BaseConfig } from './single-buffer-random.ts';
|
|
7
|
+
|
|
8
|
+
interface Config extends BaseConfig {
|
|
9
|
+
flush: number;
|
|
10
|
+
sync: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface State {
|
|
14
|
+
offsets: number[];
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
fd?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function setup(config: Config): Promise<State> {
|
|
20
|
+
await mountWithFile(config);
|
|
21
|
+
return { offsets: offsets(config), data: new Uint8Array(block).fill(0xa5) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function before(config: Config, state: State): void {
|
|
25
|
+
state.fd = fs.openSync('/f', config.sync ? 'rs+' : 'r+');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function test(config: Config, state: State) {
|
|
29
|
+
for (const offset of state.offsets) fs.writeSync(state.fd!, state.data, 0, block, offset);
|
|
30
|
+
|
|
31
|
+
if (config.flush) {
|
|
32
|
+
fs.closeSync(state.fd!);
|
|
33
|
+
state.fd = undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { writes: state.offsets.length, bytes: state.offsets.length * block };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function after(config: Config, state: State): void {
|
|
40
|
+
if (state.fd !== undefined) fs.closeSync(state.fd);
|
|
41
|
+
}
|