GitBaby/tests/pipe.rs
zhenyi 680411c4fb chore: initialize gitbaby repository
gitbaby is an asynchronous Rust library wrapping git operations, built on
gix + tokio + async-trait.

Module layout:
- archive, blame, blob, branch, cleanup, commit, compare, config,
  conflict, diff, merge, refs, remote, setup, share, submodule, tags,
  tree (feature modules)
- command: command scheduler (cmd/env/pipe/context/error)
- repo: RepositoryFacade trait (consumers implement, exposing a
  gix::Repository)

Features:
- async API on tokio 1.53
- order-preserving Env (Vec<(String, String)>)
- hand-written BabyError / CmdError, not using thiserror derive
- integration tests covering cmd_run / context / env / pipe (env keys
  prefixed for isolation)

CI: none, standard cargo build / test / clippy / fmt only
2026-08-14 18:10:04 +08:00

170 lines
4.7 KiB
Rust

use gitbaby::command::cmd::Cmd;
use gitbaby::command::context::GitPipeContext;
use gitbaby::command::pipe::Pipe;
use std::path::PathBuf;
use std::time::Duration;
fn make_cmd(caller: &str, prog: &str, arg: &str) -> Cmd {
Cmd::new(
caller,
prog,
vec![arg.to_string()],
vec![],
PathBuf::from("."),
vec![],
None,
)
}
fn must_some<T>(opt: Option<T>, msg: &str) -> T {
match opt {
Some(v) => v,
None => panic!("{msg}"),
}
}
#[test]
fn pipe_builder_sets_initial_fields() {
let pipe = Pipe::new("demo", PathBuf::from("/tmp/work"));
assert_eq!(pipe.name, "demo");
assert_eq!(pipe.dir, PathBuf::from("/tmp/work"));
assert_eq!(pipe.len(), 0);
assert!(pipe.is_empty());
assert!(!pipe.is_cancelled());
assert_eq!(pipe.timeout, None);
assert!(pipe.first().is_none());
assert!(pipe.last().is_none());
}
#[test]
fn pipe_push_grows_cmds_and_chains() {
let pipe = Pipe::new("chain", PathBuf::from("."))
.push(make_cmd("c1", "echo", "a"))
.push(make_cmd("c2", "echo", "b"));
assert_eq!(pipe.len(), 2);
assert!(!pipe.is_empty());
assert_eq!(
must_some(pipe.first(), "first must be Some").caller_info,
"c1"
);
assert_eq!(
must_some(pipe.last(), "last must be Some").caller_info,
"c2"
);
}
#[test]
fn pipe_extend_accepts_iterator() {
let cmds = vec![
make_cmd("a", "echo", "1"),
make_cmd("b", "echo", "2"),
make_cmd("c", "echo", "3"),
];
let pipe = Pipe::new("ext", PathBuf::from(".")).extend(cmds);
assert_eq!(pipe.len(), 3);
assert_eq!(must_some(pipe.last(), "last must be Some").caller_info, "c");
}
#[test]
fn pipe_with_dir_overrides_directory() {
let pipe = Pipe::new("d", PathBuf::from("/old")).with_dir(PathBuf::from("/new"));
assert_eq!(pipe.dir, PathBuf::from("/new"));
}
#[test]
fn pipe_with_timeout_sets_value() {
let pipe = Pipe::new("t", PathBuf::from(".")).with_timeout(5_000);
assert_eq!(pipe.timeout, Some(5_000));
}
#[test]
fn pipe_display_shows_name_and_joined_commands() {
let pipe = Pipe::new("pipe-name", PathBuf::from("."))
.push(make_cmd("c1", "git", "log"))
.push(make_cmd("c2", "git", "show"));
let rendered = format!("{pipe}");
assert_eq!(rendered, "pipe-name: git log | git show");
}
#[test]
fn pipe_display_when_empty() {
let pipe = Pipe::new("empty-pipe", PathBuf::from("."));
assert_eq!(format!("{pipe}"), "empty-pipe:<empty>");
}
#[test]
fn pipe_debug_includes_field_summary() {
let pipe = Pipe::new("dbg", PathBuf::from("."))
.with_timeout(42)
.push(make_cmd("c", "echo", "x"));
let rendered = format!("{pipe:?}");
assert!(rendered.contains("Pipe"));
assert!(rendered.contains("dbg"));
assert!(rendered.contains("42"));
assert!(rendered.contains("cancelled: false"));
}
#[test]
fn pipe_current_index_unset_when_empty() {
let pipe = Pipe::new("e", PathBuf::from("."));
assert_eq!(pipe.current_index(), None);
}
#[test]
fn pipe_current_index_after_push() {
let pipe = Pipe::new("e", PathBuf::from("."))
.push(make_cmd("c1", "echo", "a"))
.push(make_cmd("c2", "echo", "b"));
assert_eq!(pipe.current_index(), Some(1));
}
#[test]
fn pipe_cancel_sets_flag_and_drops_index() {
let mut pipe = Pipe::new("c", PathBuf::from("."))
.push(make_cmd("c1", "echo", "a"))
.push(make_cmd("c2", "echo", "b"));
assert!(!pipe.is_cancelled());
assert_eq!(pipe.current_index(), Some(1));
pipe.cancel_pipe();
assert!(pipe.is_cancelled());
assert_eq!(pipe.current_index(), None);
}
#[tokio::test]
async fn pipe_cancel_kills_running_children() {
let mut pipe = Pipe::new("kill", PathBuf::from(".")).push(Cmd::new(
"sleeper",
"sleep",
vec!["30".to_string()],
vec![],
PathBuf::from("."),
vec![],
None,
));
if let Err(e) = pipe.cmds[0].spawn().await {
panic!("spawn failed: {:?}", e);
}
pipe.cancel_pipe();
assert!(pipe.is_cancelled());
assert!(!pipe.spawn_allowed());
tokio::time::sleep(Duration::from_millis(200)).await;
// The child handle is handed to the reaper task; kill must be rejected
// for new spawns and the stale handle must no longer be owned by the Cmd.
assert!(
pipe.cmds[0].cmd.is_none(),
"cancel_pipe should take over the child handle"
);
}
#[tokio::test]
async fn pipe_cancel_is_safe_when_no_children_running() {
let mut pipe = Pipe::new("safe", PathBuf::from(".")).push(make_cmd("c", "echo", "x"));
pipe.cancel_pipe();
assert!(pipe.is_cancelled());
assert_eq!(pipe.current_index(), None);
}