In short: setup_test_repo creates a real jj workspace in a temporary directory. It
writes one commit with two files and a symlink, makes that commit the working-copy commit, and
reloads the repository the same way jjfsd does. The unit tests use that repository to
check the path mapper, the commits/ and workspaces/ listings, and commit
trees. The inode map and PathMappedVfs are tested with hand-built data instead. One
integration test reads the commit through the virtual file layer. The other mounts the commit for
real through FUSE and reads it with std::fs. Nothing tests src/fuse.rs or
src/main.rs directly. CI runs rustfmt, clippy, cargo check and
cargo test on pull requests and merge-queue groups. It does not run on pushes.
Run everything with cargo test --all-targets --all-features, the command CI uses. At the
pinned commit, on a Linux machine that has FUSE, it reports 30 unit tests and 2 integration tests
passing. The library target has 30 tests, the binary has none, and vfs_integration has 2.
What the tests exercise is described in inodes,
the virtual file layer,
the namespace,
commit trees and
mounting.
The throwaway repository
src/test_helpers.rs has two functions. The library pulls
it in only for tests, through #[cfg(test)] pub mod test_helpers; in
lib.rs. The integration test cannot reach a
cfg(test) module of the library, so it compiles the same file a second time with
#[path = "../src/test_helpers.rs"]. A
TODO there suggests a test-helpers Cargo feature as
the cleaner fix. tempfile is a dev-dependency only.
| Step | What it does | Source |
|---|---|---|
user_settings() | Starts from StackedConfig::with_defaults() and adds a user layer: user name "Test User", email test.user@example.com, a fixed operation username and hostname, and debug.randomness-seed = 42. | L24 |
| Workspace | Workspace::init_simple in a fresh tempfile::tempdir(). | L37 |
| Tree | A TreeBuilder on the empty tree writes file1.txt ("hello content 1") and dir/file2.txt ("hello content 2"). Both are non-executable and use CopyId::placeholder(). It also writes symlink, which points to file1.txt. | L47 |
| Commit | A backend commit with the root commit as its only parent, change id …abcd, description "test commit", and both timestamps at 0. It is written with store.write_commit. | L93 |
| Operation | A transaction runs add_head, makes the commit the default workspace's working-copy commit, and commits the operation as "set working copy". | L115 |
| Reload | Workspace::load and then repo_loader().load_at_head(), the same path main takes. | L123 |
| Returns | (TempDir, Arc<ReadonlyRepo>, CommitId). Callers keep the TempDir alive because dropping it deletes the repository. | L134 |
The repository contains three commits: the root commit, the empty working-copy commit created by
init_simple, and the test commit. test_repo_commits checks that count.
Because the helper goes through jj-lib's own
workspace and
repo code rather than mocks, the tests that use it also
check this crate against the jj-lib version it pins.
What each test reaches
Arrows show which code each test reaches. The mapper, the two listings and the commit-tree tests all
run on the repository from setup_test_repo. The InodeMap and
PathMappedVfs unit tests use hand-built data, and InodeMap is used only by
JjFuse. That makes the mounted test the only one that runs every layer together.
Unit tests by module
Inode map
| Test | Pins down | Source |
|---|---|---|
test_get_path | The root inode's path is empty, and other inodes resolve to full paths through hand-inserted parent and name entries. | L103 |
test_get_ino | New names get inodes 2 and then 3. Asking for the same name again returns the existing inode. An unknown parent (999) gives NotFound. | L138 |
test_inode_map_is_sync, …_is_send | Compile-time checks that InodeMap is Sync and Send. JjFuse shares it across tokio tasks. | L164 |
PathMappedVfs
These tests use MockVirtualFile (a file, directory or symlink with fixed contents) and a
MockPathMapper that holds a hash map, so no repository is involved.
setup_test_vfs provides file.txt
("hello world"), symlink.txt pointing to file.txt, and
dir/nested.txt.
| Test | Pins down | Source |
|---|---|---|
test_read_root_file, test_read_nested_file | read at offset 0 returns the mapped file's bytes, at the root and one directory down. | L219 |
test_list_directory | read_directory("dir") returns exactly one entry, nested.txt, typed as a file. | L233 |
test_list_directory_not_a_directory, …_not_found | Listing a file gives NotADirectory. Listing a missing path gives NotFound. | L250 |
test_read_link, test_read_link_not_a_symlink | read_link returns file.txt for the symlink and NotASymlink for a regular file. | L264 |
Path mapper and the two listings
| Test | Pins down | Source |
|---|---|---|
…_mapper_root | The empty path resolves to an entry that can be listed. | L90 |
…_commit_root, …_commit_subpath | commits/<hex> can be listed, and commits/<hex>/file1.txt can be read. Neither test checks what comes back. | L99 |
…_invalid_commit_id | A top-level name the mapper doesn't know (invalid_commit_hex) gives NotFound. Despite its name, this test never looks inside commits/. | L123 |
…_workspaces_root, …_workspace_file | workspaces lists one entry, default, and workspaces/default/file1.txt can be read (the helper made the test commit the working-copy commit). | L136 |
…_non_existent_workspace | workspaces/nonexistent and a file below it both give NotFound. | L158 |
test_repo_commits | commits/ lists three entries (the root, the initial working-copy commit and the test commit), all of them directories. | L74 |
test_repo_workspaces | workspaces/ lists exactly default, as a directory. | L63 |
Commit trees
| Test | Pins down | Source |
|---|---|---|
test_list_directory_root | The commit root lists dir, file1.txt and symlink, in that order, typed as a directory, a file and a symlink. | L187 |
test_list_directory_sub | dir lists one entry, file2.txt, typed as a file. | L206 |
test_file_size, test_read_file | file1.txt has size 15 bytes, and reading it to the end returns "hello content 1". | L221 |
test_invalid_path | some/../path gives InvalidPath for both list and read. | L245 |
test_not_found | A missing path gives NotFound for both list and read. | L274 |
test_is_directory, test_list_file_not_a_directory | Reading a directory gives NotAFile. Listing a file gives NotADirectory. | L305 |
test_file_type, test_read_symlink_file | Each entry reports the right file type, and the symlink's target reads back as file1.txt. | L333 |
Integration tests
tests/vfs_integration.rs uses the crate as an outside user
would. Both tests build the repository with setup_test_repo and wrap it in
AllCommitsPathMapper and PathMappedVfs, the same stack main
builds.
| Test | Goes through | Asserts | Source |
|---|---|---|---|
test_vfs_read_real_repo_files |
The VirtualFilesystem trait, called directly. No kernel is involved. |
commits/<hex> is a directory. file1.txt is a 15-byte file whose read returns its content. dir is a directory and dir/file2.txt is a 15-byte file. symlink is a symlink of size 9 whose target is file1.txt. |
L17 |
test_vfs_mount |
A real mount. fuser::spawn_mount runs JjFuse on a temporary directory with RO and FSName("jjfs_test"), using the multi-threaded test runtime's handle. |
It polls up to 20 × 50 ms for commits/<hex> to appear. Then it checks that std::fs reads file1.txt in full, that a 5-byte read_exact returns "hello", that dir/file2.txt reads correctly, and that symlink is a symlink pointing to file1.txt. Dropping the session at the end unmounts. |
L92 |
The mounted test is the only place where JjFuse, InodeMap and fuser's mount
path run together. A comment says the partial read guards against returning more data than was
requested, which FUSE rejects. The test needs /dev/fuse and permission to mount. Without
them, spawn_mount fails and the test panics, because the test is not skipped when FUSE is
missing.
CI workflow
.github/workflows/ci.yml runs on pull_request and
merge_group only, so a direct push to the default branch is not checked. Runs for the same
PR or merge group cancel each other, and the workflow token gets permissions: {}. Every
action is pinned by commit SHA, and checkout uses persist-credentials: false.
| Job | Runner | Command | Source |
|---|---|---|---|
check-rustfmt | ubuntu-24.04, nightly toolchain | cargo +nightly fmt --all -- --check | L18 |
check-clippy | ubuntu-24.04, stable, checks: write | cargo +stable clippy --all-features --workspace --all-targets -- -D warnings | L31 |
check | ubuntu-latest, 20-minute limit | cargo check --all-targets --all-features | L46 |
test | ubuntu-latest, 20-minute limit | cargo test --all-targets --all-features | L62 |
No step installs FUSE packages or loads the kernel module, so test_vfs_mount relies on
whatever the hosted runner image already provides. This reference has not confirmed whether it
passes there.
rustfmt settings
rustfmt.toml sets edition = "2024" and
max_width = 100. It also sets wrap_comments, format_strings,
error_on_line_overflow, group_imports = "StdExternalCrate" and
imports_granularity = "Item". Those five are unstable rustfmt options, which is why the
format job uses nightly. imports_granularity = "Item" gives every imported item its own
use line, as at the top of test_helpers.rs.
Limitations
src/fuse.rshas no unit tests. Its handlers (attribute building, inode allocation, read offsets, error-to-errno mapping) are exercised only through the one mounted test, and only on its success path.src/main.rs,static_directory.rs,jj_error.rsandvirtual_file.rshave no tests of their own.- The fixture has no conflicts, executable files, submodules or extra workspaces, so the paths for those cases (see commit trees) are untested.
test_all_commit_trees_mapper_invalid_commit_idtests an unknown top-level name, not an invalid id undercommits/.- CI does not run on pushes, and whether the mounted test can mount on GitHub's runners is unverified.