242 lines
7.1 KiB
Rust
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;
|
|
}
|