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
170 lines
4.7 KiB
Rust
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);
|
|
}
|