https://github.com/sohonetlabs/testfs
Synthetic read-only FSKit filesystem serving tree -J -s JSON trees on macOS
https://github.com/sohonetlabs/testfs
developer-tools filesystem fskit macos swift testing
Last synced: 2 months ago
JSON representation
Synthetic read-only FSKit filesystem serving tree -J -s JSON trees on macOS
- Host: GitHub
- URL: https://github.com/sohonetlabs/testfs
- Owner: sohonetlabs
- License: other
- Created: 2026-04-27T23:39:46.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-28T00:14:00.000Z (2 months ago)
- Last Synced: 2026-04-28T01:25:57.419Z (2 months ago)
- Topics: developer-tools, filesystem, fskit, macos, swift, testing
- Language: Swift
- Size: 7.07 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# TestFS
A read-only synthetic filesystem for macOS, built on **FSKit**. Mount a
JSON tree as a filesystem whose contents are made up on demand — either a
fill character or deterministic pseudo-random bytes. Useful for testing
software against arbitrarily large directory structures without spending
the disk space.
Native Swift, no kernel extension, no `fuse-t` shim. macOS 15.4 or later
(FSKit V1 is the floor).
This is the macOS-native port of
[`sohonetlabs/test_json_fs`](https://github.com/sohonetlabs/test_json_fs)
(Python + FUSE).
## Install (end users)
1. Grab the latest `TestFS-X.Y.Z.dmg` from
[Releases](https://github.com/sohonetlabs/testfs/releases).
2. Open the DMG and drag `TestFS.app` to the `Applications` shortcut.
(The DMG also contains an `Examples/` folder of sample JSON trees —
drag that to `~/Documents/` if you want them handy.)
3. Launch `/Applications/TestFS.app`. The first time, a banner asks you
to enable the FSKit extension — click **Open System Settings…**, then
toggle **TestFS** on under *General → Login Items & Extensions →
File System Extensions*.
4. Pick a JSON tree (the **File ▸ Try an example…** menu opens an
embedded set), pick an empty directory as the mountpoint, click
**Mount**. **Avoid Desktop / Documents / Downloads / iCloud
Drive / Pictures / Movies / Music** — macOS won't let `fskitd`
write to those, and the mount will fail with *Operation not
permitted*. A subdir under your home folder root or `/tmp` works.
The DMG is signed with a Developer ID Application certificate and
notarized by Apple, so Gatekeeper accepts it without warnings.
Subsequent releases install themselves: TestFS embeds
[Sparkle](https://sparkle-project.org), checks
`https://raw.githubusercontent.com/sohonetlabs/testfs/main/appcast.xml`
in the background, and prompts when a new version is available. You
can also force a check from **App ▸ Check for Updates…**.
## Build from source
```bash
git clone --recurse-submodules git@github.com:sohonetlabs/testfs.git
cd testfs
xcodebuild -project TestFS.xcodeproj -scheme TestFS \
-configuration Debug -destination 'platform=macOS' \
-allowProvisioningUpdates build
```
The build phase rsyncs `research/test_json_fs/example/` into
`TestFS.app/Contents/Resources/Examples/`, so the in-app picker works
straight from a `Debug` build.
If you don't have a Sohonet team signing identity, change
`DEVELOPMENT_TEAM` and the bundle IDs across both targets to your own.
## Mount from the CLI
Prerequisites: `TestFS.app` is installed (so the FSKit extension is
registered) and toggled on under *General → Login Items & Extensions
→ File System Extensions*. `scripts/smoke.sh` verifies both before
you bother trying to mount. Sample JSON trees live at
`research/test_json_fs/example/` — clone with `--recurse-submodules`.
```bash
scripts/smoke.sh # check the install + extension registration
scripts/mount.sh # mounts test.json at /tmp/testfs
scripts/mount.sh path/to/tree.json # custom tree, default /tmp/testfs
scripts/mount.sh path/to/tree.json /mountpoint # custom tree + mountpoint
scripts/unmount.sh # unmount + detach the dummy disk
```
Run as your normal user — **not** `sudo`. The `mount(8) -F` path that
FSKit V1 uses fails under sudo: `fskitd` checks the caller's audit
token uid against the dev node's owner, and the user-owned dev node
that `hdiutil` attached can't be opened by uid 0.
Mountpoint must be outside macOS's privacy-protected directories
(Desktop, Documents, Downloads, iCloud Drive, Pictures, Movies,
Music) — `fskitd` is denied access there and the mount fails with
*Operation not permitted*. The default `/tmp/testfs` and any fresh
subdir under your home folder root are fine.
## Architecture
```
TestFS.app
├── TestFS (SwiftUI host, unsandboxed*)
└── TestFSExtension.appex (FSKit V1 block-resource extension,
sandboxed, reads its sidecar JSON
from its own container)
```
The mounted volume is **case-sensitive** (matches the Python `jsonfs.py`
upstream). `Foo.txt` and `foo.txt` are independent entries; lookup is
byte-exact after `unicode_normalization` is applied (default NFD).
\* The host is intentionally unsandboxed because mounting in-process
needs `hdiutil` access to attach a dummy raw disk image — a path that
DiskArbitration + the IOKit user client refuse from a sandboxed
process. The FSKit extension itself is sandboxed.
## Examples
Sample JSON trees live in
[`research/test_json_fs/example/`](research/test_json_fs/example/) (a
git submodule pointing at `sohonetlabs/test_json_fs`). The
release-built DMG includes a copy of the same set under
`Examples/`. Highlights:
| File | What it demonstrates |
|---|---|
| `test.json` | 10-file basic demo |
| `bad_windows.json` / `bad_s3.json` | Names that break on those platforms |
| `big_list_of_naughty_strings_fs.json` | Unicode/edge-case fuzzing |
| `archive_torture_*.json` | Pathological inputs for archive tools |
| `imdbfslayout.json.zip` | 460k-file IMDB-shaped layout (unzip first) |
## Releasing (maintainer)
The release pipeline produces a notarized, stapled DMG containing
`TestFS.app`, an `Applications` drop-link, and the `Examples/` folder.
One-time setup on each release machine:
- `brew install create-dmg swiftlint`
- Developer ID Application certificate for team `H6XW263G62` in the
login keychain.
- Notarization keychain profile:
```bash
xcrun notarytool store-credentials testFS \
--apple-id --team-id H6XW263G62 \
--password
```
- Sparkle Ed25519 signing key. On the first machine, generate it once:
```bash
$(ls -td ~/Library/Developer/Xcode/DerivedData/TestFS-*/SourcePackages/artifacts/sparkle/Sparkle/bin | head -1)/generate_keys --account testfs
```
Back up the private key (e.g. via 1Password) — losing it means new
installs would have to ship under a different bundle ID. To restore
on a second machine, `generate_keys --account testfs -f `.
Per release:
```bash
# Bump version
echo 0.1.4 > VERSION
git commit -am "Bump version to $(cat VERSION)"
# Build, lint-gate, sign, notarize, staple, sparkle-sign, update appcast
scripts/release.sh
# ⇒ build/TestFS-0.1.4.dmg + appcast.xml updated locally
# Tag, push code, push the new appcast, upload the DMG to a Release
git tag v$(cat VERSION)
git push --follow-tags
gh release create v$(cat VERSION) build/TestFS-$(cat VERSION).dmg \
--notes "Release notes here"
git add appcast.xml && git commit -m "Appcast: $(cat VERSION)" && git push
```
`scripts/release.sh` runs `swiftlint --strict` first (cheap fail-fast
gate), then archives in Release configuration, exports with
`method=developer-id`, builds the DMG via `create-dmg`, submits to
`notarytool --wait`, staples, mounts the DMG to assess the embedded
app via `spctl`, signs the DMG with Sparkle's Ed25519 key, and
prepends the new entry to `appcast.xml`. Wall time is 3–5 minutes
depending on Apple's notarization queue.
## Reference implementation
The Python original is vendored at `research/test_json_fs/` as a git
submodule. See its README for the full feature list and CLI flags;
this Swift port aims for parity.
## License
[MIT](LICENSE). The port builds on Sohonet Labs' `test_json_fs` and
KhaosT's [`FSKitSample`](https://github.com/KhaosT/FSKitSample) — see
`LICENSE` for the attribution notices.