Testing
@zenfs/core includes the test suite it uses on itself, along with the zenfs-test script.
Any package that provides a backend can then run the entire fs conformance suite.
This is how every @zenfs package tests, and it is the fastest way to find out whether a backend you have written actually works.
Running the tests
Section titled “Running the tests”zenfs-test is a binary exposed by @zenfs/core,
so it is available through npx in any package that depends on it:
npx zenfs-test -acIt takes setup files as positional arguments. A setup file configures ZenFS with the backend under test and populates it; the runner then runs the shared suite against whatever that setup left mounted.
npx zenfs-test tests/setup-idb.ts # one specific setupnpx zenfs-test -a # every setup it can find-a/--auto discovers setup files matching **/tests/setup/*.ts and **/tests/setup-*.ts.
A setup file whose name starts with _ is skipped during auto-detection.
-c/--common additionally runs the tests that aren’t tied to any backend.
Notable options
Section titled “Notable options”| Flag | Description |
|---|---|
-a, --auto |
Auto-detect setup files. |
-c, --common |
Also run the backend-independent tests. |
-t, --test |
Only run matching suites from tests/fs, e.g. -t 'read*'. |
-s, --skip |
Skip tests whose names match a pattern. Repeatable. |
-f, --force |
Pass --test-force-exit, for backends that leave handles open. |
-e, --exit-on-fail |
Stop at the first failing suite. |
-b, --build |
Run the package’s build script first. |
-n, --node |
Run with plain node instead of tsx. Requires erasable TS syntax only. |
-v, --verbose |
Show the runner’s output instead of only pass/fail. |
-I, --inspect |
Attach the inspector, for debugging a failing test. |
-h, --help |
The full list. |
Writing a setup file
Section titled “Writing a setup file”A setup file configures ZenFS and copies in the shared
fixture data, which @zenfs/core exports from @zenfs/core/tests/setup:
import { configureSingle } from '@zenfs/core';import { copyAsync, data } from '@zenfs/core/tests/setup';import { Duck } from 'duck-backends';
await configureSingle({ backend: Duck });await copyAsync(data);data is the path to the fixture directory;
copyAsync (or copySync, for synchronous backends) copies it into the root of the virtual filesystem.
The suite assumes that content is present.
If your backend needs tearing down, you can use node:test’s hooks in the same file:
import { after } from 'node:test';
after(() => { // close connections, delete the database, and so on});A setup can also export fs to run the suite against something other than the global filesystem, like a bound context:
import { bindContext, mkdirSync } from '@zenfs/core';import { copySync, data } from '@zenfs/core/tests/setup';
mkdirSync('/new_root');
export const fs = bindContext({ root: '/new_root' }).fs;
copySync(data, fs);Test flags
Section titled “Test flags”Not every backend can do everything. A read-only archive has no writes, the File System Access API has no hard links, and a backend still being built may simply not have that feature implemented yet. A setup can declare what its backend supports, and the suite skips exactly the tests that need something missing.
Export flags from the setup file:
import type { TestFlag, TestFlagState } from '@zenfs/core/tests/common';
export const flags: Partial<Record<TestFlag, TestFlagState>> = { links: false,};Anything not listed is assumed supported, so a setup only names its exceptions.
Accepted values are true (default), false (skipped as “not supported”),
'skip' (skipped as “temporarily skipped”), and 'todo'.
| Flag | Test needs to… |
|---|---|
sync |
Use synchronous node:fs API (e.g. readFileSync) |
async |
Use callback or promises API |
write |
Modify anything. Read-only backends set this to false. |
appends |
Open files in append mode. |
directories |
Create, read, or remove directories. |
links |
Create hard links. |
symlinks |
Create or follow symbolic links. |
rename |
Rename or move. |
truncate |
Truncate a file. |
streams |
Use read/write streams. |
watch |
Use fs.watch. |
permissions |
Depend on mode bits being enforced. |
xattr |
Use extended attributes. |
times |
Depend on atime/mtime/ctime being stored and updated. |
tempdir |
Use the temporary directory. |
lchmod |
Use lchmod. |
promises.exists |
Use the non-standard exists from the promises API. |
root |
Change the global credentials. |
Tests opt in with config, which returns the skip/todo options used by node:test.
Pass every flag the test depends on:
import { config, fs } from '@zenfs/core/tests/common';import { suite, test } from 'node:test';
suite('Appends', config('appends'), () => { test('add content to an empty file', config('async'), async () => { // ... });});Coverage
Section titled “Coverage”Coverage is collected by default into tests/.coverage, and reported with c8:
npx zenfs-test -ac --report # run and reportnpx zenfs-test --report-only # report from data already collectednpx zenfs-test --clean # discard collected datanpx zenfs-test -ac -p # run, keep the data, don't report-p/--preserve is helpful when you run several invocations that should be reported together.
Each run clears the coverage directory unless it is set.
That is why @zenfs/core’s test script cleans first,
runs with -p, and reports at the end.
Performance
Section titled “Performance”The runner has two facilities for performance work.
-r/--runs runs each suite N times and prints the average,
which can be used to test for flakiness and test time variability:
npx zenfs-test -ac -r 10--profile records a V8 CPU profile per suite into .profiles/,
which can be opened in something like Chrome DevTools or VS Code:
npx zenfs-test -a --profileContinuous integration
Section titled “Continuous integration”-C/--ci reports each suite through the GitHub Checks API instead of just printing,
so individual suites show up as separate checks.
It requires @octokit/action and cannot be combined with -r.