#![allow(
clippy::print_stdout,
clippy::unwrap_used,
reason = "run with --nocapture, this test shows what Gradle printed; a step that fails fails the test"
)]
use std::path::{Path, PathBuf};
use std::process::Command;
use runnerd_core::buildcache::{BuildCache, MOUNT, SOCKET, Writes};
const IMAGE: &str = "runnerd-test-build-cache";
const GRADLE: &str = "gradle:9.8.2-jdk21-noble@sha256:12e887c27c1daaca19cab288bbe90f8fcd8d93a2a3bfb10da500a776a9d6c2c1";
fn image_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../image")
}
fn scratch(name: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("runnerd-build-cache-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn docker(args: &[&str]) -> (bool, String) {
let output = Command::new("docker").args(args).output().unwrap();
let text = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
(output.status.success(), text)
}
fn build_image() {
let context = scratch("image");
for file in ["build-cache.sh", "build-cache.gradle"] {
std::fs::copy(image_dir().join(file), context.join(file)).unwrap();
}
std::fs::write(
context.join("Dockerfile"),
format!(
"FROM {GRADLE}\n\
RUN apt-get update && apt-get install -y --no-install-recommends socat && rm -rf /var/lib/apt/lists/* \
&& useradd -m runner\n\
COPY build-cache.sh /usr/local/lib/runnerd-build-cache.sh\n\
COPY build-cache.gradle /usr/local/share/runnerd/build-cache.gradle\n\
ENV GRADLE_USER_HOME=/home/runner/.cache/gradle\n"
),
)
.unwrap();
let (built, output) = docker(&["build", "-q", "-t", IMAGE, context.to_str().unwrap()]);
std::fs::remove_dir_all(&context).unwrap();
assert!(built, "cannot build {IMAGE}:\n{output}");
}
fn project() -> PathBuf {
let dir = scratch("project");
std::fs::write(
dir.join("settings.gradle"),
"rootProject.name = 'halyard'\n",
)
.unwrap();
std::fs::write(dir.join("gradle.properties"), "org.gradle.caching=true\n").unwrap();
std::fs::write(dir.join("build.gradle"), "plugins { id 'java' }\n").unwrap();
let source = dir.join("src/main/java/halyard");
std::fs::create_dir_all(&source).unwrap();
std::fs::write(
source.join("Rigging.java"),
"package halyard;\n\npublic class Rigging {\n public static String knot() { return \"bowline\"; }\n}\n",
)
.unwrap();
dir
}
fn gradle_build(project: &Path, socket_dir: &Path) -> String {
let runtime = match std::env::var("RUNNERD_TEST_ISOLATION").as_deref() {
Ok("unsafe-dind") => "runc",
_ => "sysbox-runc",
};
let script = ". /usr/local/lib/runnerd-build-cache.sh && start_build_cache \
&& cp -r /project /work && chown -R runner /work \
&& setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner \
gradle --no-daemon --console=plain -p /work compileJava";
let (built, output) = docker(&[
"run",
"--rm",
"--runtime",
runtime,
"-v",
&format!("{}:/project:ro", project.display()),
"--mount",
&format!(
"type=bind,source={},target={MOUNT},readonly",
socket_dir.display()
),
IMAGE,
"bash",
"-c",
script,
]);
println!("{output}");
assert!(built, "the build failed:\n{output}");
output
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs Docker, sysbox and the network"]
async fn a_main_builds_outputs_are_from_cache_in_a_fresh_container() {
tokio::task::spawn_blocking(build_image).await.unwrap();
let project = project();
let root = scratch("cache");
let cache = BuildCache::open(&root.join("entries"), &root.join("sockets"), 1 << 30).unwrap();
let main = cache.attach("main", "halyard/api").unwrap();
cache.decide("main", Writes::Allowed);
let output = tokio::task::spawn_blocking({
let (project, main) = (project.clone(), main.clone());
move || gradle_build(&project, &main)
})
.await
.unwrap();
assert!(
!output.contains("FROM-CACHE"),
"the first build has nothing to take"
);
let stored = cache.detach("main").unwrap();
assert!(stored.written_bytes > 0, "main stored nothing: {stored:?}");
let pull = cache.attach("pull", "halyard/api").unwrap();
cache.decide("pull", Writes::Refused);
let output = tokio::task::spawn_blocking({
let (project, pull) = (project.clone(), pull.clone());
move || gradle_build(&project, &pull)
})
.await
.unwrap();
assert!(
output.contains("Task :compileJava FROM-CACHE"),
"compileJava ran again:\n{output}"
);
let read = cache.detach("pull").unwrap();
assert!(read.hits >= 1, "{read:?}");
let down = root.join("down");
std::fs::create_dir_all(&down).unwrap();
drop(std::os::unix::net::UnixListener::bind(down.join(SOCKET)).unwrap());
std::fs::set_permissions(
down.join(SOCKET),
std::os::unix::fs::PermissionsExt::from_mode(0o666),
)
.unwrap();
let output = tokio::task::spawn_blocking({
let project = project.clone();
move || gradle_build(&project, &down)
})
.await
.unwrap();
assert!(output.contains("BUILD SUCCESSFUL"), "{output}");
assert!(!output.contains("FROM-CACHE"), "{output}");
std::fs::remove_dir_all(&project).unwrap();
std::fs::remove_dir_all(&root).unwrap();
}