Development & Testing
All toolchains are managed via mise (mise.toml, Rust pinned).
mise install # install Rust 1.97.1
Engine + CLI (natively on macOS)
wusel-core and wusel (without the fuse feature) build without a FUSE driver:
mise run check # cargo check (wusel-core + wusel, no FUSE)
mise run test # cargo test
mise run clippy
Testing strategy
A pyramid — we do not boot a Nextcloud for every run.
-
Unit tests (no server). Pure logic: PROPFIND parsing, SQLite state, sync-state decisions, three-way merge, path handling. Fast, deterministic, run everywhere.
-
Purpose-built fake servers (no Nextcloud, no mocking crate). Protocols that are not WebDAV get a few lines of hand-rolled
tokioserver in the test itself: thenotify_pushwebsocket and the self-signed-TLS handshake. No HTTP-mocking dependency is pulled in for this — see the dependency policy in Architecture. -
Standalone WebDAV mock (
wusel-mock). A tiny binary crate that serves a real directory as a Nextcloud-style user root (PROPFIND Depth 0/1, range GET, and the write verbs). It is pure Rust with a single dependency (tokio) — deliberately no Python and no mocking framework, to keep the project to one language and few dependencies. Because it mirrors a directory on disk, exercising change detection is justecho/rmin that directory, and fixtures are staged by creating files rather than by writing XML. Two fault-injection markers in a file’s name provoke server behaviour that is otherwise hard to stage:.fail-once(the firstPUTanswers 500) and.no-etag*(PUT/MOVEanswer without anETag). Start it withmise run mock(--root DIR), point a mount at it, or let the integration tests undercrates/wusel-mock/tests/drive the real client against it — that is where every server-facing engine test lives.
All three tiers run under mise run test — they are ordinary cargo test cases
(the standalone mock via an integration test; wusel-mock is also a library, so
tests can drive it in-process).
|
On Linux It buys two things. The credential tests answer the same on an unlocked GNOME desktop and in a bare container, instead of depending on whether the machine happens to have a Secret Service. And no test can touch your own login keyring — the same account key the product uses. See Credential storage in Architecture for how the keyring’s failure modes are covered without a keyring at all. |
-
Real FUSE mount (Linux,
/dev/fuse).mise run fuse-testmounts the full stack — engine + FUSE — against the in-processwusel-mockand drives it through the kernel:ls,cat,stat,statfs, then unmount. It needs a Linux container, so it is not part of the nativemise run test; run it via podman (see below). This is the automated counterpart to the manual mount check. -
Real Nextcloud — automated, nightly.
scripts/e2e-nextcloud.shdrives a FUSE-mountedwuselagainst a real Nextcloud container: read/hydration, write/upload, pin, and the opt-in 3-way merge on a genuine 412 conflict. This is the tier a mock cannot stand in for (ETag propagation, chunked-upload NG, the server’s own conditional-request behaviour). Run it locally withmise run e2e-local(podman); in CI it is the E2E workflow, see below. Anything the script still cannot cover — livenotify_push, the desktop integration in a real session — stays a manual check against our own instance (see Trying it out).
Continuous integration (GitHub Actions)
Four workflows under .github/workflows/. All of them go through mise, so CI
runs the same pinned toolchain and the same mise run … tasks as a local
checkout — there is no separate CI script to keep in sync.
| Workflow | Trigger | What it does |
|---|---|---|
CI ( |
every push to |
Formatting, licence headers, clippy ( |
E2E ( |
nightly, plus manual dispatch |
The real-Nextcloud end-to-end run above. It is gated on the code having changed since the last successful run, so a docs-only day costs nothing. |
Docs ( |
every push to |
Builds this Antora site; pushes to |
Release ( |
a |
Builds the Fedora RPM for |
Before pushing, mise run fmt-check, mise run clippy, mise run check and
mise run test reproduce the CI job locally.
Documentation diagrams
The diagrams under documentation/diagrams/*.d2 are rendered to committed
SVGs, and the docs build embeds those — it never runs d2, which is why the
Pages workflow installs nothing but antora. The workflow after editing one:
mise run docs-diagrams # re-render, then commit the changed SVGs
Forgetting that step used to be invisible: the site would build green and ship a
picture that no longer matched the page around it. It is now caught, and without
adding a d2 dependency to the build — mise run docs-diagrams records the
hash of every source in diagrams/rendered.sha256, and
documentation/check-diagrams.sh compares them. The docs build calls it
(refusing an official build, warning in watch mode), so CI does too.
Sources beginning with an underscore are shared partials imported with
…@name, not diagrams — they are not rendered, but they are hashed, since a
change to _steps.d2 invalidates every diagram that imports it.
Mounting on Linux (podman container)
FUSE needs a kernel driver → Linux container with /dev/fuse:
mise run fuse-build # build wusel including FUSE
mise run fuse-test # run the real-mount e2e test (headless)
mise run fuse-shell # interactive shell with /dev/fuse
# in the shell:
mise exec -- cargo run -p wusel --features fuse -- mount /mnt/nc
|
The
|
Building from an external disk (one-time VM setup)
To let the VM see a repo on an external disk (e.g. /Volumes/<disk>),
share the disk into the podman machine — two steps, surviving VM restarts:
-
Add the disk to the
Mountsarray of~/.config/containers/podman/machine/libkrun/podman-machine-default.json(copy an existing entry;Source/Target=/Volumes/<disk>, a uniqueTag), thenpodman machine stop && podman machine start. -
Mount it inside the guest (Fedora CoreOS:
/is immutable, so the target is/var/mnt; the SELinux context matches podman’s own shares — without it, containers getPermission denied):podman machine ssh "sudo mkdir -p /var/mnt/<disk> && \ echo '<tag> /var/mnt/<disk> virtiofs rw,nofail,context=system_u:object_r:nfs_t:s0 0 0' \ | sudo tee -a /etc/fstab && sudo systemctl daemon-reload && sudo mount /var/mnt/<disk>"
The VM’s disk is a sparse file: it grows as data is written and does not
shrink when files inside are deleted, so podman system prune alone leaves the
host file at its high-water mark. To hand the space back, trim inside the guest:
podman machine ssh "sudo fstrim -av". If a container build ever fails, check
/var/tmp in the guest too — podman leaves the unpacked build context behind
and nothing collects it.
|
podman machine init recreates the VM from scratch — redo both steps then (the
scripts fall back to mirroring automatically until you do).
The mount is Linux-only, so on macOS use the same podman container. Native macOS support (a File Provider frontend, not FUSE) is far-future, experimental work. See Trying it out for a step-by-step guide.
Release build (Linux)
To run wusel as a normal binary instead of through cargo, build it in release
mode on Linux (the fuse feature needs the kernel driver):
mise run build-release # cargo build --release --features fuse -p wusel
The binary is then at target/release/wusel — run it from there, or put it
on your PATH wherever you like. For end users the distribution format is the
Fedora RPM (packaging/rpm/, built by mise run rpm); a Debian package is not
built yet — see Project, licence & distribution.
The file manager’s cloud-provider registration is a separate step (it goes into a
system data dir, which a file manager scans but ~/.local/share is not), run as
root: wusel desktop install-provider. For the full file-manager story
(sidebar status, per-file emblems, context-menu actions, and the build
prerequisites for the native extension), see
File-manager integration.