Back to feed

sledtools/pika branch #122

pika-git-test-fixtures-4

pika-git: factor test fixtures

Target branch: master

Merge Commit: 591a51a10e7b8335acc41f03d61de1b8fe7cc3f3

branch: merged tutorial: ready ci: failed
Open CI Details

Continuous Integration

CI: failed

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #149 failed

2 failed

head d753f0fa8f84424b530a37afd778354b2c5ec64e · queued 2026-03-26 22:10:11 · 2 lane(s)

queued 1m 00s · ran 28s

check-notifications · failed check-agent-contracts · failed

Summary

This branch extracts repetitive git test-fixture boilerplate from four test modules (ci, mirror, poller, web) into a single shared test_support::GitTestRepo struct. Every test previously created its own tempdir, bare repo, seed checkout, and configured git identity manually — roughly 10–12 lines of identical setup per test. The new GitTestRepo helper centralises that into a one-liner (GitTestRepo::new()) and exposes ergonomic methods (write_seed, seed_add, seed_commit, seed_push_master, seed_push_branch, seed_checkout_new_branch, chmod_seed_executable, open_store) that replace scattered fs::write, git(…), and Store::open calls. A secondary WebTestContext helper is introduced in web.rs for web-specific tests that only need a store and no git repo. The net effect is roughly 200 lines deleted and substantially improved readability across all test sites.

Tutorial Steps

Add the shared `test_support` module

Intent: Create a new `test_support.rs` module containing the `GitTestRepo` struct that encapsulates tempdir creation, bare-repo initialisation, seed-checkout initialisation, and git-identity configuration — the boilerplate common to every integration test.

Affected files: crates/pika-git/src/test_support.rs, crates/pika-git/src/main.rs

Evidence
@@ -0,0 +1,117 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use crate::storage::Store;
+
+pub(crate) struct GitTestRepo {
+    _root: tempfile::TempDir,
+    bare: PathBuf,
+    seed: PathBuf,
+    db_path: PathBuf,
+}
@@ -20,6 +20,8 @@ mod pikaci_store;
 mod poller;
 mod render;
 mod storage;
+#[cfg(test)]
+mod test_support;

The core of this branch is the new crates/pika-git/src/test_support.rs module, gated behind #[cfg(test)] in main.rs.

GitTestRepo::new() performs the full setup that was previously duplicated in every test:

  1. Creates a tempfile::TempDir (held as _root to keep it alive).
  2. Initialises a bare repo at <root>/pika.git.
  3. Initialises a seed (working) checkout at <root>/seed.
  4. Configures user.name and user.email in the seed repo.
  5. Derives a db_path at <root>/pika-git.db.

Accessor methods expose the paths:

  • bare_path() -> &Path
  • seed_path() -> &Path
  • db_path() -> &Path

Convenience methods replace the free-standing git(…) helpers and inline fs calls:

  • write_seed(relative, contents) — creates parent dirs and writes a file into the seed checkout.
  • chmod_seed_executable(relative)chmod 0o755 on a seed file.
  • seed_add(paths) — stages files.
  • seed_commit(message) — commits.
  • seed_push_master() — renames branch to master, adds origin remote, and pushes.
  • seed_checkout_new_branch(name) / seed_push_branch(name) — for feature-branch workflows.
  • open_store() — opens a Store at the repo's db_path.
  • seed_git(args) — escape hatch for arbitrary git commands in the seed dir.

Migrate `ci.rs` tests to `GitTestRepo`

Intent: Replace the 10–12-line boilerplate block in each of the eight CI tests with a single `GitTestRepo::new()` call plus the accessor methods, and switch `&seed`/`&bare` references to the borrowed `&Path` returned by the struct.

Affected files: crates/pika-git/src/ci.rs

Evidence
@@ -636,6 +636,7 @@ mod tests {
     use crate::ci_state::CiLaneStatus;
     use crate::config::{Config, ForgeRepoConfig};
     use crate::storage::Store;
+    use crate::test_support::GitTestRepo;
@@ -709,18 +710,10 @@ mod tests {
 
     #[test]
     fn nightly_schedule_creates_durable_history_once_per_slot() {
-        let root = tempfile::tempdir().expect("create temp root");
-        let bare = root.path().join("pika.git");
-        let seed = root.path().join("seed");
-        let db_path = root.path().join("pika-git.db");
-
-        git(
-            root.path(),
-            &["init", "--bare", bare.to_str().expect("bare path")],
-        );
-        git(root.path(), &["init", seed.to_str().expect("seed path")]);
-        git(&seed, &["config", "user.name", "Test User"]);
-        git(&seed, &["config", "user.email", "test@example.com"]);
+        let repo = GitTestRepo::new();
+        let bare = repo.bare_path();
+        let seed = repo.seed_path();
+        let db_path = repo.db_path();

All eight test functions in ci.rs follow the same pattern. The diff shows each one replacing the manual tempdir + git init --bare + git init + git config block with:

let repo = GitTestRepo::new();
let bare = repo.bare_path();
let seed = repo.seed_path();
let db_path = repo.db_path();

Because the accessors return &Path (not &PathBuf), the downstream git(…) calls in ci.rs change from git(&seed, …) to git(seed, …) — removing unnecessary double-borrows. Similarly, Store::open(&db_path) becomes Store::open(db_path). The module-level git() free function is retained in ci.rs because these tests use it for mid-test git operations that don't map to a single GitTestRepo convenience method (e.g., adding specific files, pushing specific branches). The boilerplate removal saves roughly 10 lines per test × 8 tests = ~80 lines.

Migrate `mirror.rs` tests and remove local `git` helper

Intent: Replace the duplicated git helper function and manual repo setup in the three mirror tests with `GitTestRepo`, and use the new convenience methods (`write_seed`, `seed_add`, `seed_commit`, `seed_push_master`, `open_store`) to simplify the test bodies further.

Affected files: crates/pika-git/src/mirror.rs

Evidence
@@ -212,21 +212,7 @@ mod tests {
 
     use super::{run_background_mirror_pass, run_mirror_pass};
     use crate::config::{Config, ForgeRepoConfig};
-    use crate::storage::Store;
-
-    fn git<P: AsRef<std::path::Path>>(cwd: P, args: &[&str]) {
-        let output = Command::new("git")
-            .args(args)
-            .current_dir(cwd.as_ref())
-            .output()
-            .expect("run git");
-        assert!(
-            output.status.success(),
-            "git {:?} failed: {}",
-            args,
-            String::from_utf8_lossy(&output.stderr)
-        );
-    }
+    use crate::test_support::GitTestRepo;
@@ -244,43 +230,36 @@ mod tests {
     fn mirror_pass_records_success_and_zero_lag() {
-        let root = tempfile::tempdir().expect("create temp root");
+        let repo = GitTestRepo::new();
+        repo.write_seed("README.md", "hello\\n");
+        repo.seed_add(&["README.md"]);
+        repo.seed_commit("initial");
+        repo.seed_push_master();

The mirror tests go a step further than ci.rs: the local fn git() helper is entirely removed from the module, replaced by the GitTestRepo methods.

The mirror_pass_records_success_and_zero_lag and background_mirror_pass_respects_configured_interval tests both need an additional mirror bare repo. Since GitTestRepo doesn't model a second bare repo, these tests create it manually via Command::new("git") — a reasonable escape hatch since the mirror repo is domain-specific to these tests.

The mirror_pass_records_failure_for_bad_remote test becomes significantly shorter because it doesn't need a seed checkout at all — the GitTestRepo::new() constructor provides the bare repo and open_store() provides the database.

Migrate `poller.rs` tests and remove local `git` helper

Intent: Replace the boilerplate and per-module `git` helper in poller tests with `GitTestRepo` convenience methods, demonstrating the full range of helpers including `chmod_seed_executable`, `seed_checkout_new_branch`, and `seed_push_branch`.

Affected files: crates/pika-git/src/poller.rs

Evidence
@@ -125,28 +125,12 @@ pub fn poll_once_limited_with_updates(
 
 #[cfg(test)]
 mod tests {
-    use std::fs;
-    use std::process::Command;
-
     use crate::ci_state::CiLaneStatus;
     use crate::config::{Config, ForgeRepoConfig};
+    use crate::test_support::GitTestRepo;
 
     use super::poll_once_limited;
-    use crate::{ci, storage::Store};
-
-    fn git<P: AsRef<std::path::Path>>(cwd: P, args: &[&str]) {
@@ -164,40 +148,21 @@ mod tests {
     fn branch_push_queues_lanes_from_branch_head_manifest() {
-        let root = tempfile::tempdir().expect("create temp root");
+        let repo = GitTestRepo::new();
+        repo.write_seed("README.md", "hello\\n");
+        repo.write_seed(
+            "lane-a.sh",
+            "#!/usr/bin/env bash\\nset -euo pipefail\\necho lane-a\\n",
+        );
         for script in ["lane-a.sh", "lane-b.sh"] {
-            let mut perms = fs::metadata(seed.join(script))
+            repo.chmod_seed_executable(script);
         }

The poller tests showcase the full convenience API. The branch_push_queues_lanes_from_branch_head_manifest test demonstrates:

  • repo.write_seed(path, contents) replacing fs::write(seed.join(…), …)
  • repo.chmod_seed_executable(script) replacing the manual PermissionsExt dance
  • repo.seed_checkout_new_branch("feature/manifest") replacing git(&seed, &["checkout", "-b", …])
  • repo.seed_push_branch("feature/manifest") replacing git(&seed, &["push", "origin", …])

The invalid_branch_manifest_records_failed_suite_for_head test follows the same pattern. Both tests also drop the use std::fs and use std::process::Command imports since those are now encapsulated in the helper.

The module's local fn git() helper and the use crate::storage::Store import are removed entirely.

Migrate `web.rs` tests and introduce `WebTestContext`

Intent: Replace the duplicated tempdir + Store::open boilerplate in web handler tests with a lightweight `WebTestContext` struct, and use `GitTestRepo` for the one web test that needs a full git repo.

Affected files: crates/pika-git/src/web.rs

Evidence
@@ -3730,7 +3730,6 @@ async fn shutdown_signal() {
 #[cfg(test)]
 mod tests {
     use std::fs;
-    use std::os::unix::fs::PermissionsExt;
@@ -3844,6 +3830,32 @@ mod tests {
+    struct WebTestContext {
+        _dir: tempfile::TempDir,
+        store: Store,
+    }
+
+    impl WebTestContext {
+        fn new() -> Self {
+            let dir = tempfile::tempdir().expect("create temp dir");
+            let db_path = dir.path().join("pika-git.db");
+            let store = Store::open(&db_path).expect("open store");
+            Self { _dir: dir, store }
+        }
@@ -5943,27 +5945,14 @@ oldsha newsha refs/tags/v1
     fn merged_branch_page_renders_after_source_branch_deletion() {
-        let root = tempfile::tempdir().expect("create temp root");
+        let repo = GitTestRepo::new();
+        repo.write_seed("README.md", "hello\\n");

The web tests split into two categories:

Store-only tests (webhook verification, inbox listing, review marking, branch resolution, auth challenge) use the new WebTestContext:

let ctx = WebTestContext::new();
let store = ctx.store();
// … test body …
let state = ctx.state(forge_test_config());
let headers = ctx.trusted_headers(TRUSTED_NPUB);

WebTestContext wraps a TempDir and a Store, and provides store(), state(config), and trusted_headers(npub) convenience methods. This replaces the recurring 3-line tempdir → db_path → Store::open block across ~10 async tests.

Git-repo tests (merged_branch_page_renders_after_source_branch_deletion) use GitTestRepo directly, leveraging write_seed, chmod_seed_executable, seed_add, seed_commit, seed_push_master, seed_checkout_new_branch, and seed_push_branch.

The module-level fn git() helper and the use std::os::unix::fs::PermissionsExt import are both removed since they're now handled by GitTestRepo.

Diff