Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions crates/codegraph-cli/tests/cli_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,45 @@ fn opener(dir: &TestDir) -> (PathBuf, PathBuf) {
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
wait_until_executable(&script);
// The warm-up run wrote an empty marker; the tests read only what `codegraph ui` writes.
let _ = std::fs::remove_file(&marker);
script
};
(script, marker)
}

/// Run a just-written executable once, retrying while the kernel reports it busy.
///
/// Another thread of this test binary may fork while the script is still open for writing; the
/// child holds the inherited descriptor until it execs, and running the script meanwhile fails with
/// ETXTBSY, so `codegraph ui` could not start the stand-in browser. Once one run succeeds no writer
/// is left.
#[cfg(not(windows))]
fn wait_until_executable(script: &Path) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match std::process::Command::new(script)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(mut child) => {
let _ = child.wait();
return;
}
Err(error)
if error.kind() == std::io::ErrorKind::ExecutableFileBusy
&& Instant::now() < deadline =>
{
std::thread::sleep(Duration::from_millis(2));
}
Err(error) => panic!("cannot run {}: {error}", script.display()),
}
}
}

fn wait_for_file(path: &Path, timeout: Duration) -> Option<String> {
let deadline = Instant::now() + timeout;
loop {
Expand Down
31 changes: 31 additions & 0 deletions crates/codegraph-watch/src/git_pending.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,9 +755,40 @@ mod tests {
)
.unwrap();
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
wait_until_executable(&script);
script
}

/// Run a just-written executable once, retrying while the kernel reports it busy.
///
/// Another thread of this test binary may fork while the script is still open for writing;
/// the child holds the inherited descriptor until it execs, and running the script meanwhile
/// fails with ETXTBSY, which `git` reports as a failed git (main's Coverage job on 81bd7c7).
/// Once one run succeeds no writer is left, so the test's own runs cannot hit it.
fn wait_until_executable(script: &Path) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match std::process::Command::new(script)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(mut child) => {
let _ = child.wait();
return;
}
Err(error)
if error.kind() == std::io::ErrorKind::ExecutableFileBusy
&& Instant::now() < deadline =>
{
std::thread::sleep(Duration::from_millis(2));
}
Err(error) => panic!("cannot run {}: {error}", script.display()),
}
}
}

#[test]
fn a_system_attributes_file_is_a_conversion_risk() {
let dir = std::env::temp_dir().join(format!("cg_fake_git_{}", std::process::id()));
Expand Down
40 changes: 39 additions & 1 deletion crates/codegraph-watch/tests/git_pending_fast_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,38 @@ fn symlinks_decline() {
fs::remove_dir_all(&outside).ok();
}

/// Start a just-written executable once, retrying while the kernel reports it busy, then stop it.
///
/// Another thread of this test binary may fork while the file is still open for writing; the
/// child holds the inherited descriptor until it execs, and running the file meanwhile fails with
/// ETXTBSY. Here that failure looked like a declined git, so the test passed without the timeout
/// ever running. Once one start succeeds no writer is left.
#[cfg(unix)]
fn wait_until_executable(path: &Path) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match std::process::Command::new(path)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(mut child) => {
let _ = child.kill();
let _ = child.wait();
return;
}
Err(error)
if error.kind() == std::io::ErrorKind::ExecutableFileBusy
&& Instant::now() < deadline =>
{
std::thread::sleep(Duration::from_millis(2));
}
Err(error) => panic!("cannot run {}: {error}", path.display()),
}
}
}

#[cfg(unix)]
#[test]
fn a_hanging_git_declines_within_the_timeout() {
Expand All @@ -625,15 +657,21 @@ fn a_hanging_git_declines_within_the_timeout() {
return;
};
let fake = repo.top.with_extension("fake-git");
fs::write(&fake, "#!/bin/sh\nsleep 30\n").unwrap();
// `exec`, so killing the stand-in kills the sleep too and leaves no orphan behind.
fs::write(&fake, "#!/bin/sh\nexec sleep 30\n").unwrap();
fs::set_permissions(&fake, fs::Permissions::from_mode(0o755)).unwrap();
wait_until_executable(&fake);
git_pending_hooks::set_git_program(Some(fake.clone()));
git_pending_hooks::set_git_timeout(Some(Duration::from_millis(300)));
let started = Instant::now();
let declined = repo.pending();
git_pending_hooks::set_git_program(None);
git_pending_hooks::set_git_timeout(None);
assert_eq!(declined.1, PendingSource::FullInventory);
assert!(
started.elapsed() >= Duration::from_millis(300),
"the stand-in git ran until the timeout, rather than failing to start"
);
assert!(
started.elapsed() < Duration::from_secs(20),
"the timeout bounds the wait"
Expand Down
Loading