Files
sausage/host/tests/it/git_runtime.rs
T

242 lines
7.1 KiB
Rust

//! Git stays responsive through the shell, and cancellation removes only its own index lock.
use sausage::{
exec::{Command, Exec, Options},
process::Runtime,
world::World,
};
use sausage_vfs::{Layer, Open};
use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
time::Duration,
};
fn guests() -> &'static Path {
static BUILT: OnceLock<PathBuf> = OnceLock::new();
BUILT.get_or_init(|| {
let target = crate::common::build_git();
assert!(crate::common::build_brush().unwrap().success());
target.join("wasm32-wasip2/release")
})
}
fn native(cwd: &Path, args: &[&str]) {
let output = std::process::Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
struct Fixture {
_temp: tempfile::TempDir,
runtime: Arc<Runtime>,
root: Arc<dyn Layer>,
processes: sausage::process::Processes,
}
fn fixture() -> Fixture {
let programs = guests();
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("world");
for path in ["bin", "src/demo/src", "worktrees"] {
std::fs::create_dir_all(root.join(path)).unwrap();
}
for (source, name) in [("brush", "sh"), ("git", "git")] {
std::fs::copy(
programs.join(format!("{source}.wasm")),
root.join(format!("bin/{name}.wasm")),
)
.unwrap();
}
let repo = root.join("src/demo");
std::fs::write(repo.join("src/main.rs"), "old\n").unwrap();
native(&repo, &["init", "-b", "main"]);
native(&repo, &["add", "."]);
native(
&repo,
&[
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"commit",
"--no-gpg-sign",
"-m",
"initial",
],
);
native(
&repo,
&[
"worktree",
"add",
"-b",
"worker",
root.join("worktrees/160").to_str().unwrap(),
],
);
std::fs::write(
root.join("worktrees/160/.git"),
"gitdir: /src/demo/.git/worktrees/160\n",
)
.unwrap();
std::fs::write(
repo.join(".git/worktrees/160/gitdir"),
"/worktrees/160/.git\n",
)
.unwrap();
let world = World::open(temp.path()).unwrap();
let root = world.root.clone();
let processes = world.processes.clone();
let runtime = Runtime::new(world).unwrap();
Fixture {
_temp: temp,
runtime,
root,
processes,
}
}
fn options() -> Options {
Options {
cwd: "/worktrees/160".into(),
env: [
("GIT_AUTHOR_NAME", "Test"),
("GIT_AUTHOR_EMAIL", "test@example.com"),
("GIT_COMMITTER_NAME", "Test"),
("GIT_COMMITTER_EMAIL", "test@example.com"),
]
.map(|(k, v)| (k.into(), v.into()))
.into(),
..Default::default()
}
}
async fn run(fixture: &Fixture, script: &str) -> String {
let mut output = Vec::new();
let code = fixture
.runtime
.exec(Command::Shell(script.into()), options(), &mut |_, bytes| {
output.extend_from_slice(bytes)
})
.await
.unwrap();
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&output));
String::from_utf8(output).unwrap()
}
#[tokio::test(flavor = "current_thread")]
async fn conversation_bash_commit_inherits_nearest_attribution() {
let fixture = fixture();
let mut options = options();
options.conversations = vec!["158".into(), "79".into()];
options
.env
.push(("SAUSAGE_CONVERSATION".into(), "wrong".into()));
let mut output = Vec::new();
let code = fixture
.runtime
.exec(
Command::Shell("sh -c 'git commit --allow-empty --no-gpg-sign -m attributed'".into()),
options,
&mut |_, bytes| output.extend_from_slice(bytes),
)
.await
.unwrap();
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&output));
let commit = std::process::Command::new("git")
.args(["log", "worker", "-1", "--format=%B"])
.current_dir(fixture._temp.path().join("world/src/demo"))
.output()
.unwrap();
assert!(commit.status.success());
let text = String::from_utf8(commit.stdout).unwrap();
assert!(text.contains("Sausage-Conversation: 158"), "{text}");
assert!(!text.contains("Sausage-Conversation: 79"), "{text}");
assert!(!text.contains("Sausage-Conversation: wrong"), "{text}");
}
async fn edit(fixture: &Fixture) {
let file = fixture
.root
.open(
"/worktrees/160/src/main.rs",
Open {
write: true,
truncate: true,
..Default::default()
},
)
.await
.unwrap();
file.write(0, b"changed\n".to_vec()).await.unwrap();
}
#[cfg(not(debug_assertions))]
#[tokio::test(flavor = "current_thread")]
async fn linked_worktree_commands_stay_under_two_seconds() {
let fixture = fixture();
run(&fixture, "git status --short").await;
edit(&fixture).await;
for script in [
"git status --short",
"git log -1",
"git add src/main.rs && git commit --no-gpg-sign -m changed",
] {
let start = std::time::Instant::now();
let output = run(&fixture, script).await;
let elapsed = start.elapsed();
println!("{script}: {elapsed:?}\n{output}");
assert!(elapsed < Duration::from_secs(2), "{script}: {elapsed:?}");
}
}
#[tokio::test(flavor = "current_thread")]
async fn cancelled_add_releases_its_index_lock() {
let fixture = fixture();
run(&fixture, "git status --short").await;
edit(&fixture).await;
let path = "/src/demo/.git/worktrees/160/index.lock";
let mut changes = fixture
.root
.observer()
.unwrap()
.subscribe(vec![path.into()]);
let mut output = Vec::new();
let mut collect = |_, bytes: &[u8]| output.extend_from_slice(bytes);
let mut command = Box::pin(fixture.runtime.exec(
Command::Argv(vec![
"/bin/git.wasm".into(),
"add".into(),
"src/main.rs".into(),
]),
options(),
&mut collect,
));
tokio::time::timeout(Duration::from_secs(30), async {
tokio::select! {
biased;
change = changes.recv() => { assert_eq!(change.unwrap().0, path); },
result = &mut command => panic!("add completed before cancellation: {result:?}"),
}
})
.await
.unwrap();
assert!(
!fixture.processes.lock().unwrap().is_empty(),
"Git must still run when cancellation occurs"
);
drop(command);
tokio::time::timeout(Duration::from_secs(30), async {
while !fixture.processes.lock().unwrap().is_empty() {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert!(fixture.root.stat(path, false).await.is_err());
run(
&fixture,
"git add src/main.rs && git commit --no-gpg-sign -m recovered",
)
.await;
}