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:
- Creates a
tempfile::TempDir (held as _root to keep it alive).
- Initialises a bare repo at
<root>/pika.git.
- Initialises a seed (working) checkout at
<root>/seed.
- Configures
user.name and user.email in the seed repo.
- 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.