#![cfg(unix)]
use std::ffi::OsStr;
use std::io::Read;
use std::process::Command;
use strop_core::worker::CancelToken;
use strop_git::{GitExec, GitExecError};
use strop_workspace::ContainerId;
fn required() -> bool {
std::env::var_os("STROP_CONTAINER_TESTS").as_deref() == Some(OsStr::new("1"))
}
fn engine_available() -> bool {
Command::new("docker")
.arg("info")
.output()
.is_ok_and(|output| output.status.success())
}
fn gate(test: &str) -> bool {
if !required() {
eprintln!("skipping {test}: STROP_CONTAINER_TESTS is not 1");
return false;
}
if !engine_available() {
eprintln!("skipping {test}: STROP_CONTAINER_TESTS=1 but the docker engine is unreachable");
return false;
}
true
}
fn with_token<T>(work: impl FnOnce(CancelToken) -> T) -> T {
let (tokens, receiver) = std::sync::mpsc::channel();
let (release, waiting) = std::sync::mpsc::channel::<()>();
let owner = strop_core::worker::spawn(
"git-container-test",
|_| {},
move |token| {
tokens.send(token).expect("test receives token");
let _ = waiting.recv();
strop_core::worker::Outcome::Success(())
},
);
let token = receiver.recv().expect("worker issued token");
let result = work(token);
drop(release);
drop(owner);
result
}
fn tag() -> String {
let mut bytes = [0u8; 8];
std::fs::File::open("/dev/urandom")
.and_then(|mut file| file.read_exact(&mut bytes))
.expect("urandom is available on unix");
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
struct Fixture {
id: String,
}
impl Fixture {
fn launch(tag: &str, name: &str) -> Fixture {
let output = Command::new("docker")
.args([
"run",
"-d",
"--label",
&format!("strop-test-run={tag}"),
"--name",
name,
"busybox",
"sleep",
"300",
])
.output()
.expect("docker CLI runs");
assert!(
output.status.success(),
"docker run failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Fixture {
id: String::from_utf8(output.stdout)
.expect("docker answers UTF-8")
.trim()
.to_string(),
}
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = Command::new("docker").args(["rm", "-f", &self.id]).output();
}
}
#[test]
fn missing_git_in_container_fails_typed_never_hangs() {
if !gate("missing_git_in_container_fails_typed_never_hangs") {
return;
}
let tag = tag();
let name = format!("strop-dc1b-git-{tag}");
let fixture = Fixture::launch(&tag, &name);
let id = ContainerId::canonical(fixture.id.clone()).expect("run printed the canonical id");
let workdir = std::path::PathBuf::from("/");
let exec = GitExec::Container {
container: id,
workdir: &workdir,
};
let argv: Vec<std::ffi::OsString> = vec!["status".into()];
match with_token(|token| exec.run(&argv, &token)) {
Err(GitExecError::Container(error)) => {
eprintln!("missing git surfaced as a boundary error: {error}");
}
Ok(run) => {
assert!(!run.success, "busybox has no git: {run:?}");
assert_ne!(run.code, Some(0));
}
Err(other) => panic!("unexpected backend error: {other}"),
}
}
#[test]
fn discover_without_git_fails_typed_never_hangs() {
if !gate("discover_without_git_fails_typed_never_hangs") {
return;
}
let tag = tag();
let name = format!("strop-dc1b-discover-{tag}");
let fixture = Fixture::launch(&tag, &name);
let id = ContainerId::canonical(fixture.id.clone()).expect("run printed the canonical id");
let from = std::path::PathBuf::from("/");
match with_token(|token| strop_git::container::discover(&id, &from, &token)) {
Err(error) => {
eprintln!("missing git surfaced as a typed refusal: {error}");
}
Ok(found) => panic!("busybox has no git: {found:?}"),
}
}