//! `ATGC_GIT_LOG` — that it writes where it is pointed, and that `off` means //! off. //! //! Driven through the real binary because that is the only way to set the //! variable at all: `std::env::set_var` is unsafe and this crate forbids //! unsafe, so a unit test cannot put a value in the environment of its own //! process. `Command::env` on a *child* is neither unsafe nor shared, which //! is the same reason `tests/exit_status.rs` redirects `HOME` this way. //! //! What the log records once it is open is covered where it belongs, beside //! the runner that emits it (`clients/git/run.rs`). What is only checkable //! from out here is the switch. use std::path::Path; use std::process::Command; use tempfile::TempDir; /// A git repository with no remote, and the process pointed at it. /// /// `pr list` in one runs git — `git remote get-url origin`, and the two /// narrowing questions behind the error it produces — and then stops, with /// no session read and no socket opened. That is the cheapest command in the /// tree that certainly spawns git. /// /// Built under a canonicalized temp root, so the repository's path has no /// symlinks in it and git's own answers about where it is agree with ours. /// Deleted when the returned value drops. fn scratch_repo(name: &str) -> TempDir { let root = std::fs::canonicalize(std::env::temp_dir()).expect("a path with no symlinks in it"); let dir = tempfile::Builder::new() .prefix(&format!("atgc-gitlog-{name}-")) .tempdir_in(root) .expect("a temp directory"); let init = Command::new("git") .current_dir(dir.path()) .args(["init", "-q", "-b", "main"]) .status() .expect("git is a hard requirement of this suite"); assert!(init.success()); dir } fn atgc_in(dir: &Path, log: &str, args: &[&str]) { let out = Command::new(env!("CARGO_BIN_EXE_atgc")) .current_dir(dir) .args(args) // Nothing here may read or write the real config directory. The two // other logs are switched off outright; this one is the subject. .env("HOME", "/nonexistent-atgc-test-home") .env("ATGC_OAUTH_LOG", "off") .env("ATGC_PDS_LOG", "off") .env("ATGC_GIT_LOG", log) .env("ATGC_NO_INPUT", "1") .env("NO_COLOR", "1") .output() .expect("spawn atgc"); // The command itself is expected to fail — there is no remote — and its // status is `exit_status.rs`'s subject, not this file's. let _ = out.status; } /// Pointed at a path, the log lands there: the invocation head line, and a /// `spawn` for a git subprocess that really ran. #[test] fn the_variable_moves_the_log_and_a_subprocess_is_recorded() { let dir = scratch_repo("on"); let log = dir.path().join("elsewhere.jsonl"); atgc_in(dir.path(), &log.to_string_lossy(), &["pr", "list"]); let text = std::fs::read_to_string(&log).expect("the log was written where it was pointed"); assert!( text.lines().any(|l| l.contains(r#""event":"invocation""#)), "{text}" ); let spawn = text .lines() .find(|l| l.contains(r#""event":"spawn""#)) .expect("a git subprocess was recorded"); assert!(spawn.contains(r#""event":"spawn""#)); // Every line carries the envelope the other two logs use, which is what // lets the three be read side by side. let parsed: serde_json::Value = serde_json::from_str(spawn).expect("a JSON line"); assert!(parsed["inv"].is_string()); assert!(parsed["ts"].is_string()); assert!(parsed["seq"].is_number()); assert!( text.lines().any(|l| l.contains(r#""event":"exit""#)), "a spawn is paired with an exit: {text}" ); } /// `off` means off: not an empty file, no file. /// /// The variable is checked before anything is opened, so a machine that has /// switched this log off pays neither the file nor the `HEAD` probes that go /// with it. #[test] fn off_really_disables_it() { let dir = scratch_repo("off"); let log = dir.path().join("elsewhere.jsonl"); for value in ["off", "0", ""] { let _ = std::fs::remove_file(&log); atgc_in(dir.path(), value, &["pr", "list"]); assert!( !log.exists(), "ATGC_GIT_LOG={value:?} still wrote {}", log.display() ); } }