jj-vfs-poc / Tests and CI

Tests and CI

Thirty unit tests, two integration tests and four CI jobs. Most of the tests run against a real jj repository that is built in a temporary directory for each test.

On this page
  1. The throwaway repository
  2. What each test reaches
  3. Unit tests by module
  4. Inode map
  5. PathMappedVfs
  6. Path mapper and the two listings
  7. Commit trees
  8. Integration tests
  9. CI workflow
  10. rustfmt settings
  11. Limitations

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.

StepWhat it doesSource
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
WorkspaceWorkspace::init_simple in a fresh tempfile::tempdir().L37
TreeA 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
CommitA 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
OperationA transaction runs add_head, makes the commit the default workspace's working-copy commit, and commits the operation as "set working copy".L115
ReloadWorkspace::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

Test coverage by layer setup_test_repo builds a real jj repository, which is used by the path mapper, directory and commit tree unit tests and by both integration tests. The inode map and PathMappedVfs unit tests use hand-built data instead. test_vfs_read_real_repo_files goes through PathMappedVfs and the real path mapper. test_vfs_mount goes through the kernel, fuser and JjFuse as well, and JjFuse uses the InodeMap. JjFuse and main have no tests of their own. test_vfs_mount std::fs over a real FUSE mount kernel · fuser · JjFuse no unit tests test_vfs_read_real_repo_files calls the VFS trait directly PathMappedVfs 7 tests on a mock mapper InodeMap 4 tests, hand-built entries AllCommitsPathMapper 7 tests commits/ · 1 test workspaces/ · 1 test CommitTreeFile · 10 tests setup_test_repo() real jj workspace in a tempdir: 2 files, 1 symlink, 3 commits

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

TestPins downSource
test_get_pathThe root inode's path is empty, and other inodes resolve to full paths through hand-inserted parent and name entries.L103
test_get_inoNew 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_sendCompile-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.

TestPins downSource
test_read_root_file, test_read_nested_fileread at offset 0 returns the mapped file's bytes, at the root and one directory down.L219
test_list_directoryread_directory("dir") returns exactly one entry, nested.txt, typed as a file.L233
test_list_directory_not_a_directory, …_not_foundListing a file gives NotADirectory. Listing a missing path gives NotFound.L250
test_read_link, test_read_link_not_a_symlinkread_link returns file.txt for the symlink and NotASymlink for a regular file.L264

Path mapper and the two listings

TestPins downSource
…_mapper_rootThe empty path resolves to an entry that can be listed.L90
…_commit_root, …_commit_subpathcommits/<hex> can be listed, and commits/<hex>/file1.txt can be read. Neither test checks what comes back.L99
…_invalid_commit_idA 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_fileworkspaces 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_workspaceworkspaces/nonexistent and a file below it both give NotFound.L158
test_repo_commitscommits/ lists three entries (the root, the initial working-copy commit and the test commit), all of them directories.L74
test_repo_workspacesworkspaces/ lists exactly default, as a directory.L63

Commit trees

TestPins downSource
test_list_directory_rootThe commit root lists dir, file1.txt and symlink, in that order, typed as a directory, a file and a symlink.L187
test_list_directory_subdir lists one entry, file2.txt, typed as a file.L206
test_file_size, test_read_filefile1.txt has size 15 bytes, and reading it to the end returns "hello content 1".L221
test_invalid_pathsome/../path gives InvalidPath for both list and read.L245
test_not_foundA missing path gives NotFound for both list and read.L274
test_is_directory, test_list_file_not_a_directoryReading a directory gives NotAFile. Listing a file gives NotADirectory.L305
test_file_type, test_read_symlink_fileEach 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.

TestGoes throughAssertsSource
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.

JobRunnerCommandSource
check-rustfmtubuntu-24.04, nightly toolchaincargo +nightly fmt --all -- --checkL18
check-clippyubuntu-24.04, stable, checks: writecargo +stable clippy --all-features --workspace --all-targets -- -D warningsL31
checkubuntu-latest, 20-minute limitcargo check --all-targets --all-featuresL46
testubuntu-latest, 20-minute limitcargo test --all-targets --all-featuresL62

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.rs has 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.rs and virtual_file.rs have 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_id tests an unknown top-level name, not an invalid id under commits/.
  • CI does not run on pushes, and whether the mounted test can mount on GitHub's runners is unverified.