#![cfg(all(feature = "exec-subprocess", feature = "exec-boxlite"))]
use std::path::Path;
use std::process::Command;
use rto_exec::{
FeatureSet, Guidance, GuidanceLine as Line, LintConfigGrant, LintRequested, decide_lint_host,
};
use rto_graph::Isolation;
const IMAGE_VAR: &str = "ROTEIRO_TEST_LINT_IMAGE";
const PREFETCH: &str =
"roteiro security prefetch --analyzer clippy --allow-download --image $ROTEIRO_TEST_LINT_IMAGE";
const MAIN: &str = r#"fn main() {
let v: Vec<i32> = vec![1, 2, 3];
for i in 0..v.len() {
println!("{}", v[i]);
}
}
"#;
const MANIFEST: &str = r#"[package]
name = "sandboxed-builder-fixture"
version = "0.1.0"
edition = "2021"
"#;
#[test]
fn a_sandboxed_lint_reports_from_inside_the_boundary_and_leaves_the_tree_alone() {
let Some(image) = preconditions() else {
return;
};
let tree = fixture();
let root = tree.path();
let before = digest_tree(root);
let outcome = rto_exec::run_lint(
"clippy",
root,
&FeatureSet::Defaults,
decide_lint_host(LintConfigGrant::default(), LintRequested::Unset),
Some(&image),
)
.expect("a sandboxed lint");
assert_eq!(outcome.isolation, Isolation::MicroVm);
assert_eq!(
outcome.image.as_deref(),
Some(image.as_str()),
"the run must name the image it was actually put inside"
);
assert!(
outcome.toolchain.rustc.starts_with("rustc "),
"the guest's rustc was not read: {:?}",
outcome.toolchain
);
assert!(
outcome.toolchain.host.contains("linux"),
"the guest is a Linux microVM, whatever this host is: {:?}",
outcome.toolchain
);
assert!(
outcome.command.iter().any(|a| a == "--offline"),
"a guest with no interface must be told so: {:?}",
outcome.command
);
assert!(
outcome.summary.build_succeeded,
"the fixture must actually compile, or the assertions below prove nothing"
);
assert!(
outcome
.report
.findings
.iter()
.any(|f| f.rule.contains("needless_range_loop")),
"the fixture's diagnostic was not reported: {:?}",
outcome.report.findings
);
assert!(
!outcome.scratch.starts_with(root),
"the build wrote inside the tree it was reviewing: {}",
outcome.scratch.display()
);
assert!(
!root.join("target").exists(),
"a `target/` was left in the tree under review"
);
assert_eq!(
before,
digest_tree(root),
"the sandboxed lint modified the tree it was reporting on"
);
assert!(
outcome.scratch.join("debug").is_dir(),
"nothing was built in {}",
outcome.scratch.display()
);
}
fn preconditions() -> Option<String> {
let Ok(image) = std::env::var(IMAGE_VAR) else {
eprintln!(
"SKIPPED: {IMAGE_VAR} is unset, so there is no image to lint inside.{}",
Guidance::new(&[
Line::Note(&[
"Roteiro ships no default and will not choose one — no first-party Rust",
"image carries the `clippy` component (rust-lang/docker-rust builds every",
"stable and nightly variant `--profile minimal`), and picking a third",
"party's would make somebody else's container the boundary.",
]),
Line::Note(&["See docs/SANDBOXED_LINTING.md for the Dockerfile, then:"]),
Line::Command(
"export ROTEIRO_TEST_LINT_IMAGE=registry/you/rust-clippy@sha256:<64 hex>"
),
Line::Command(PREFETCH),
])
);
return None;
};
match rto_exec::sandbox_probe() {
rto_exec::SandboxProbe::Available => {}
rto_exec::SandboxProbe::Unavailable(why) => {
eprintln!(
"SKIPPED: no microVM is available on this host, so a sandboxed build cannot \
run: {why}\n (the expected state on a CI runner with no /dev/kvm)"
);
return None;
}
}
match rto_exec::boxlite::reference_is_present(IMAGE_VAR, &image, &rto_exec::asset_root()) {
Ok(true) => {}
Ok(false) => {
eprintln!(
"SKIPPED: {image} is not in the local image store, and a run never pulls.{}",
Guidance::new(&[
Line::Note(&["Provisioning fetches; running reads. Pull it first:"]),
Line::Command(PREFETCH),
])
);
return None;
}
Err(e) => {
eprintln!("SKIPPED: the local image store could not be read: {e}");
return None;
}
}
Some(image)
}
struct FixtureTree(std::path::PathBuf);
impl FixtureTree {
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for FixtureTree {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
fn fixture() -> FixtureTree {
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT: AtomicUsize = AtomicUsize::new(0);
let dir = std::env::temp_dir().join(format!(
"rto-exec-sandboxed-builder-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(dir.join("src")).expect("src");
std::fs::write(dir.join("Cargo.toml"), MANIFEST).expect("manifest");
std::fs::write(dir.join("src/main.rs"), MAIN).expect("main");
let generated = Command::new("cargo")
.args(["generate-lockfile", "--quiet"])
.current_dir(&dir)
.output();
assert!(
matches!(&generated, Ok(out) if out.status.success()),
"the fixture needs a lockfile, because `roteiro lint` passes --locked: {generated:?}"
);
FixtureTree(dir)
}
fn digest_tree(root: &Path) -> String {
let mut entries: Vec<(String, String)> = Vec::new();
walk(root, root, &mut entries);
entries.sort();
let mut joined = String::new();
for (path, digest) in &entries {
use std::fmt::Write as _;
let _ = writeln!(joined, "{path} {digest}");
}
rto_exec::sha256_hex(joined.as_bytes())
}
fn walk(root: &Path, dir: &Path, out: &mut Vec<(String, String)>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(root, &path, out);
} else if let Ok(bytes) = std::fs::read(&path) {
let relative = path
.strip_prefix(root)
.unwrap_or(&path)
.display()
.to_string();
out.push((relative, rto_exec::sha256_hex(&bytes)));
}
}
}