use std::fs;
use std::io;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use cargoless_proto::{
ArtifactMeta, BuildIdentity, BuildOutcome, BuildResult, BuildTrigger, Profile,
PublishedArtifact, TargetTriple, UnixSeconds,
};
use cargoless_cas::{ContentStore, absent_marker, content_hash, hash_source_tree, input_hash};
pub trait Compiler {
fn compile(&self, project_root: &Path, identity: &BuildIdentity) -> Result<Vec<u8>, String>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct TrunkCompiler;
impl Compiler for TrunkCompiler {
fn compile(&self, project_root: &Path, _identity: &BuildIdentity) -> Result<Vec<u8>, String> {
let output = Command::new("trunk")
.arg("build")
.current_dir(project_root)
.output()
.map_err(|e| format!("could not launch `trunk build`: {e}"))?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(extract_trunk_failure_reason(
&stdout,
&stderr,
output.status.code(),
));
}
let dist = project_root.join("dist");
if !dist.is_dir() {
return Err("`trunk build` succeeded but produced no dist/ directory".to_owned());
}
pack_dir(&dist).map_err(|e| format!("could not read trunk dist/: {e}"))
}
}
pub(crate) fn extract_trunk_failure_reason(
stdout: &str,
stderr: &str,
exit_code: Option<i32>,
) -> String {
let exit_label = match exit_code {
Some(c) => format!("exit {c}"),
None => "signal".to_owned(),
};
let error_lines: Vec<String> = stderr
.lines()
.chain(stdout.lines())
.map(str::trim)
.filter(|l| !l.is_empty())
.filter(|l| is_error_prefix_line(l))
.map(str::to_owned)
.collect();
if !error_lines.is_empty() {
let take_n = error_lines.len().min(5);
let summary = error_lines[error_lines.len() - take_n..].join(" | ");
return format!("`trunk build` {exit_label} — {summary}");
}
let stderr_tail: Vec<&str> = stderr
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.collect();
if stderr_tail.is_empty() {
return format!(
"`trunk build` {exit_label} with no diagnostic output \
(run `trunk build` directly to investigate)"
);
}
let take_n = stderr_tail.len().min(3);
let context = stderr_tail[stderr_tail.len() - take_n..].join(" | ");
format!(
"`trunk build` {exit_label} — no canonical `error:` line found; \
last {take_n} stderr line(s): {context}"
)
}
fn is_error_prefix_line(line: &str) -> bool {
let Some(first) = line.split_whitespace().next() else {
return false;
};
let first_lc = first.to_ascii_lowercase();
first_lc == "error:"
|| first_lc.starts_with("error[")
|| first_lc == "error"
|| first == "ERROR"
}
const DIST_BLOB_HEADER: &[u8] = b"tf-core/dist/v1\n";
fn pack_dir(root: &Path) -> io::Result<Vec<u8>> {
fn walk(root: &Path, dir: &Path, out: &mut Vec<(String, Vec<u8>)>) -> io::Result<()> {
let mut kids: Vec<fs::DirEntry> = fs::read_dir(dir)?.collect::<io::Result<Vec<_>>>()?;
kids.sort_by_key(std::fs::DirEntry::file_name);
for k in kids {
let path = k.path();
let meta = fs::symlink_metadata(&path)?;
if meta.is_dir() {
walk(root, &path, out)?;
} else if meta.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<String>>()
.join("/");
out.push((rel, fs::read(&path)?));
}
}
Ok(())
}
let mut files = Vec::new();
walk(root, root, &mut files)?;
files.sort_by(|a, b| a.0.cmp(&b.0));
let mut buf = Vec::new();
buf.extend_from_slice(DIST_BLOB_HEADER);
for (rel, bytes) in &files {
buf.extend_from_slice(&(rel.len() as u64).to_be_bytes());
buf.extend_from_slice(rel.as_bytes());
buf.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
buf.extend_from_slice(bytes);
}
Ok(buf)
}
pub fn unpack_artifact(blob: &[u8], out_dir: &Path) -> io::Result<()> {
let bad = |m: &str| io::Error::new(io::ErrorKind::InvalidData, m.to_string());
let mut cur = blob
.strip_prefix(DIST_BLOB_HEADER)
.ok_or_else(|| bad("not a cargoless dist blob (bad/absent header)"))?;
let take = |cur: &mut &[u8], n: usize| -> io::Result<Vec<u8>> {
if cur.len() < n {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"artifact blob truncated",
));
}
let (head, tail) = cur.split_at(n);
*cur = tail;
Ok(head.to_vec())
};
let take_u64 = |cur: &mut &[u8]| -> io::Result<u64> {
Ok(u64::from_be_bytes(
take(cur, 8)?
.try_into()
.map_err(|_| bad("short length field"))?,
))
};
while !cur.is_empty() {
let rel_len =
usize::try_from(take_u64(&mut cur)?).map_err(|_| bad("path length exceeds usize"))?;
let rel = String::from_utf8(take(&mut cur, rel_len)?)
.map_err(|_| bad("entry path is not UTF-8"))?;
let content_len = usize::try_from(take_u64(&mut cur)?)
.map_err(|_| bad("content length exceeds usize"))?;
let content = take(&mut cur, content_len)?;
let mut dest = out_dir.to_path_buf();
for seg in rel.split('/') {
if seg.is_empty() || seg == "." || seg == ".." {
return Err(bad("unsafe component in artifact path"));
}
dest.push(seg);
}
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dest, &content)?;
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Materialized {
NoGreen,
Evicted(PublishedArtifact),
Materialized(PublishedArtifact),
}
pub fn materialize_latest_green<S: ContentStore>(
store: &S,
project_root: &Path,
out_dir: &Path,
) -> io::Result<Materialized> {
let Some(pa) = read_latest_green(project_root)? else {
return Ok(Materialized::NoGreen);
};
match store.get(&pa.artifact.input_hash)? {
None => Ok(Materialized::Evicted(pa)),
Some(blob) => {
unpack_artifact(&blob, out_dir)?;
Ok(Materialized::Materialized(pa))
}
}
}
fn hash_optional_file(path: &Path, kind: &str) -> io::Result<cargoless_proto::ContentHash> {
match fs::read(path) {
Ok(bytes) => Ok(content_hash(&bytes)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(absent_marker(kind)),
Err(e) => Err(e),
}
}
pub fn assemble_identity(
project_root: impl AsRef<Path>,
target: TargetTriple,
profile: Profile,
) -> io::Result<BuildIdentity> {
let root = project_root.as_ref();
Ok(BuildIdentity {
source_tree: hash_source_tree(root)?,
cargo_lock: hash_optional_file(&root.join("Cargo.lock"), "cargo_lock")?,
rust_toolchain: hash_optional_file(&root.join("rust-toolchain.toml"), "rust_toolchain")?,
tf_config: hash_optional_file(&root.join("tf.toml"), "tf_config")?,
target,
profile,
})
}
pub struct BuildOrchestrator<S: ContentStore, C: Compiler> {
store: S,
compiler: C,
project_root: PathBuf,
}
impl<S: ContentStore, C: Compiler> BuildOrchestrator<S, C> {
pub fn new(store: S, compiler: C, project_root: impl Into<PathBuf>) -> Self {
Self {
store,
compiler,
project_root: project_root.into(),
}
}
pub fn run(&self, trigger: &BuildTrigger) -> BuildResult {
let key = input_hash(&trigger.identity);
let meta = ArtifactMeta {
input_hash: key.clone(),
identity: trigger.identity.clone(),
};
match self.store.contains(&key) {
Ok(true) => return self.publish_and_report(BuildOutcome::Deduplicated, meta),
Ok(false) => {}
Err(e) => return failed(format!("CAS lookup failed: {e}")),
}
let bytes = match self.compiler.compile(&self.project_root, &trigger.identity) {
Ok(b) => b,
Err(reason) => return failed(reason),
};
if let Err(e) = self.store.put(&key, &bytes) {
return failed(format!("CAS store failed: {e}"));
}
match self.store.contains(&key) {
Ok(true) => {}
Ok(false) => {
return failed("CAS reported store success but artifact is absent".to_owned());
}
Err(e) => return failed(format!("CAS verify failed: {e}")),
}
self.publish_and_report(BuildOutcome::Compiled, meta)
}
fn publish_and_report(&self, outcome: BuildOutcome, meta: ArtifactMeta) -> BuildResult {
match publish_latest_green(&self.project_root, &meta) {
Ok(()) => BuildResult {
outcome,
artifact: Some(meta),
},
Err(e) => failed(format!(
"artifact built but could not advance latest-green pointer: {e}"
)),
}
}
}
pub fn latest_green_path(project_root: &Path) -> PathBuf {
project_root.join(".cargoless").join("latest-green")
}
pub fn read_latest_green(project_root: &Path) -> io::Result<Option<PublishedArtifact>> {
match fs::read_to_string(latest_green_path(project_root)) {
Ok(text) => PublishedArtifact::parse(&text)
.map(Some)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn publish_latest_green(project_root: &Path, meta: &ArtifactMeta) -> io::Result<()> {
let published_at = UnixSeconds(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
let record = PublishedArtifact {
artifact: meta.clone(),
published_at,
};
let dir = project_root.join(".cargoless");
fs::create_dir_all(&dir)?;
let tmp = dir.join(format!(".latest-green.{}.tmp", std::process::id()));
{
let mut f = fs::File::create(&tmp)?;
f.write_all(record.render().as_bytes())?;
f.sync_all()?;
}
match fs::rename(&tmp, dir.join("latest-green")) {
Ok(()) => Ok(()),
Err(e) => {
let _ = fs::remove_file(&tmp);
Err(e)
}
}
}
fn failed(reason: String) -> BuildResult {
BuildResult {
outcome: BuildOutcome::Failed { reason },
artifact: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use cargoless_cas::LocalDiskStore;
use std::cell::Cell;
use std::path::PathBuf;
fn scratch(tag: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("cargoless-core-build-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&p);
fs::create_dir_all(&p).unwrap();
p
}
struct CountingCompiler {
calls: Cell<usize>,
bytes: Vec<u8>,
}
impl Compiler for CountingCompiler {
fn compile(&self, _root: &Path, _id: &BuildIdentity) -> Result<Vec<u8>, String> {
self.calls.set(self.calls.get() + 1);
Ok(self.bytes.clone())
}
}
fn ident() -> BuildIdentity {
BuildIdentity {
source_tree: cargoless_cas::content_hash(b"s"),
cargo_lock: cargoless_cas::content_hash(b"l"),
rust_toolchain: cargoless_cas::content_hash(b"t"),
tf_config: cargoless_cas::content_hash(b"c"),
target: TargetTriple::new("wasm32-unknown-unknown"),
profile: Profile::Dev,
}
}
#[test]
fn extract_reason_finds_cargo_error_lines_in_stderr() {
let stderr = "\
Compiling foo v0.1.0\n\
error[E0277]: the trait bound `T: Foo` is not satisfied\n\
--> src/lib.rs:10:5\n\
error: could not compile `foo` (lib) due to 1 previous error\n";
let got = extract_trunk_failure_reason("", stderr, Some(101));
assert!(got.contains("E0277"), "rustc code surfaced: {got}");
assert!(got.contains("could not compile"));
assert!(got.contains("exit 101"));
assert!(
!got.contains("Compiling foo"),
"non-error line leaked: {got}"
);
}
#[test]
fn extract_reason_finds_trunk_error_lines() {
let stderr = "\
INFO starting build\n\
ERROR ❌ wasm-bindgen-cli not found\n\
ERROR ❌ aborting due to previous error\n";
let got = extract_trunk_failure_reason("", stderr, Some(1));
assert!(got.contains("wasm-bindgen-cli not found"));
assert!(got.contains("exit 1"));
assert!(!got.contains("INFO"), "INFO line leaked: {got}");
}
#[test]
fn extract_reason_f12_dogfood_smoking_gun_does_not_surface_finished_line() {
let stderr = "\
Compiling dogfood-realapp v0.1.0\n\
ERROR ❌ wasm-bindgen-cli missing — install via `cargo install wasm-bindgen-cli`\n\
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s\n";
let got = extract_trunk_failure_reason("", stderr, Some(1));
let surfaced_as_reason = got.ends_with("in 0.16s")
|| (got.contains("Finished") && !got.contains("wasm-bindgen-cli"));
assert!(
!surfaced_as_reason,
"cargo success line was surfaced as failure reason: {got}"
);
assert!(
got.contains("wasm-bindgen-cli missing"),
"real error must be surfaced: {got}"
);
}
#[test]
fn extract_reason_scans_both_stdout_and_stderr() {
let stdout = "error: linking failed via lld\n";
let stderr = "Compiling foo\nFinished `dev` profile in 1.2s\n";
let got = extract_trunk_failure_reason(stdout, stderr, Some(1));
assert!(
got.contains("linking failed"),
"stdout error surfaced: {got}"
);
assert!(!got.ends_with("in 1.2s"));
}
#[test]
fn extract_reason_fallback_when_no_error_lines() {
let stderr = "step 1: prepare\nstep 2: compile\nFinished `dev` profile in 0.5s\n";
let got = extract_trunk_failure_reason("", stderr, Some(2));
assert!(got.contains("exit 2"));
assert!(got.contains("no canonical `error:` line found"));
assert!(got.contains("step 1: prepare"));
assert!(got.contains("step 2: compile"));
assert!(got.contains("Finished"));
}
#[test]
fn extract_reason_handles_empty_output() {
let got = extract_trunk_failure_reason("", "", None);
assert!(got.contains("signal"), "signal flagged: {got}");
assert!(got.contains("no diagnostic output"));
}
#[test]
fn extract_reason_caps_at_five_error_lines_for_readability() {
let stderr: String = (1..=20).fold(String::new(), |mut s, i| {
use std::fmt::Write as _;
let _ = writeln!(s, "error: failure #{i}# marker");
s
});
let got = extract_trunk_failure_reason("", &stderr, Some(101));
for i in 16..=20 {
let marker = format!("#{i}#");
assert!(got.contains(&marker), "expected marker {marker} in: {got}");
}
for i in 1..=15 {
let marker = format!("#{i}#");
assert!(
!got.contains(&marker),
"earlier marker {marker} leaked into: {got}"
);
}
}
#[test]
fn is_error_prefix_line_negative_cases() {
assert!(!is_error_prefix_line(
"Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s"
));
assert!(!is_error_prefix_line("Compiling dogfood-realapp v0.1.0"));
assert!(!is_error_prefix_line("Building target dist/..."));
assert!(!is_error_prefix_line("warning: unused import"));
assert!(!is_error_prefix_line(
"summary: 3 errors during compilation"
));
assert!(!is_error_prefix_line(""));
assert!(!is_error_prefix_line(" "));
}
#[test]
fn is_error_prefix_line_positive_cases() {
assert!(is_error_prefix_line("error: linking failed"));
assert!(is_error_prefix_line("error[E0277]: trait bound"));
assert!(is_error_prefix_line("error[unused_imports]: …"));
assert!(is_error_prefix_line("ERROR ❌ wasm-bindgen-cli not found"));
assert!(is_error_prefix_line("ERROR aborting"));
}
#[test]
fn first_build_compiles_second_is_deduplicated() {
let dir = scratch("dedupe");
let store = LocalDiskStore::new(dir.join("cas"));
let compiler = CountingCompiler {
calls: Cell::new(0),
bytes: b"artifact".to_vec(),
};
let orch = BuildOrchestrator::new(store, compiler, &dir);
let trig = BuildTrigger { identity: ident() };
let r1 = orch.run(&trig);
assert_eq!(r1.outcome, BuildOutcome::Compiled);
assert!(r1.artifact.is_some());
let r2 = orch.run(&trig);
assert_eq!(
r2.outcome,
BuildOutcome::Deduplicated,
"identical identity ⇒ cache hit"
);
assert_eq!(
orch.compiler.calls.get(),
1,
"the compile MUST be skipped on the dedupe path (AC#5)"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn compile_failure_holds_last_green() {
struct Boom;
impl Compiler for Boom {
fn compile(&self, _r: &Path, _i: &BuildIdentity) -> Result<Vec<u8>, String> {
Err("linker exploded".to_owned())
}
}
let dir = scratch("fail");
let orch = BuildOrchestrator::new(LocalDiskStore::new(dir.join("cas")), Boom, &dir);
let r = orch.run(&BuildTrigger { identity: ident() });
assert_eq!(
r.outcome,
BuildOutcome::Failed {
reason: "linker exploded".to_owned()
}
);
assert!(
r.artifact.is_none(),
"no artifact ⇒ server holds last-green"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn assemble_identity_is_stable_and_revert_sensitive() {
let proj = scratch("assemble");
fs::create_dir_all(proj.join("src")).unwrap();
fs::write(proj.join("src/main.rs"), b"fn main() {}").unwrap();
fs::write(proj.join("Cargo.lock"), b"# lock").unwrap();
let id1 = assemble_identity(
&proj,
TargetTriple::new("wasm32-unknown-unknown"),
Profile::Dev,
)
.unwrap();
let id_again = assemble_identity(
&proj,
TargetTriple::new("wasm32-unknown-unknown"),
Profile::Dev,
)
.unwrap();
assert_eq!(id1, id_again, "unchanged tree ⇒ identical identity");
fs::write(proj.join("src/main.rs"), b"fn main() { /* x */ }").unwrap();
let id2 = assemble_identity(
&proj,
TargetTriple::new("wasm32-unknown-unknown"),
Profile::Dev,
)
.unwrap();
assert_ne!(id1, id2, "a source edit ⇒ different identity");
fs::write(proj.join("src/main.rs"), b"fn main() {}").unwrap();
let id3 = assemble_identity(
&proj,
TargetTriple::new("wasm32-unknown-unknown"),
Profile::Dev,
)
.unwrap();
assert_eq!(id1, id3, "revert ⇒ original identity returns");
assert!(!proj.join("tf.toml").exists());
let _ = fs::remove_dir_all(&proj);
}
#[test]
fn compiled_and_deduplicated_both_advance_the_pointer() {
let dir = scratch("publish");
let orch = BuildOrchestrator::new(
LocalDiskStore::new(dir.join("cas")),
CountingCompiler {
calls: Cell::new(0),
bytes: b"artifact".to_vec(),
},
&dir,
);
let trig = BuildTrigger { identity: ident() };
assert_eq!(orch.run(&trig).outcome, BuildOutcome::Compiled);
let p1 = read_latest_green(&dir)
.unwrap()
.expect("pointer advanced on first green build");
assert_eq!(p1.artifact.input_hash, input_hash(&ident()));
assert!(p1.published_at.0 > 0, "a real timestamp is recorded");
assert_eq!(orch.run(&trig).outcome, BuildOutcome::Deduplicated);
assert_eq!(orch.compiler.calls.get(), 1, "AC#5: compile still skipped");
let p2 = read_latest_green(&dir).unwrap().expect("pointer present");
assert_eq!(p2.artifact.input_hash, p1.artifact.input_hash);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn failed_build_never_moves_the_pointer() {
struct Boom;
impl Compiler for Boom {
fn compile(&self, _r: &Path, _i: &BuildIdentity) -> Result<Vec<u8>, String> {
Err("trunk exploded".to_owned())
}
}
let dir = scratch("nopublish");
let good = BuildOrchestrator::new(
LocalDiskStore::new(dir.join("cas")),
CountingCompiler {
calls: Cell::new(0),
bytes: b"green-1".to_vec(),
},
&dir,
);
assert_eq!(
good.run(&BuildTrigger { identity: ident() }).outcome,
BuildOutcome::Compiled
);
let before = fs::read(latest_green_path(&dir)).expect("pointer exists");
let mut other = ident();
other.source_tree = cargoless_cas::content_hash(b"different-source");
let bad = BuildOrchestrator::new(LocalDiskStore::new(dir.join("cas")), Boom, &dir);
let r = bad.run(&BuildTrigger { identity: other });
assert!(matches!(r.outcome, BuildOutcome::Failed { .. }));
assert!(r.artifact.is_none());
let after = fs::read(latest_green_path(&dir)).expect("pointer still exists");
assert_eq!(
before, after,
"AC#4: a failed build leaves the pointer byte-identical"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn pointer_write_failure_is_failed_closed_option_a() {
let dir = scratch("ptrfail");
fs::write(dir.join(".cargoless"), b"not a directory").unwrap();
let store = LocalDiskStore::new(dir.join("cas"));
let orch = BuildOrchestrator::new(
store,
CountingCompiler {
calls: Cell::new(0),
bytes: b"artifact".to_vec(),
},
&dir,
);
let r = orch.run(&BuildTrigger { identity: ident() });
match r.outcome {
BuildOutcome::Failed { reason } => {
assert!(
reason.contains("could not advance latest-green pointer"),
"reason names the publish failure: {reason}"
);
}
other => panic!("expected Failed, got {other:?}"),
}
assert!(
r.artifact.is_none(),
"fail closed: no servable artifact reported"
);
assert!(
LocalDiskStore::new(dir.join("cas"))
.contains(&input_hash(&ident()))
.unwrap(),
"artifact is safely cached despite the publish failure"
);
let _ = fs::remove_dir_all(&dir);
}
struct DistCompiler {
blob: Vec<u8>,
}
impl Compiler for DistCompiler {
fn compile(&self, _r: &Path, _i: &BuildIdentity) -> Result<Vec<u8>, String> {
Ok(self.blob.clone())
}
}
fn make_dist(tag: &str) -> (PathBuf, Vec<u8>) {
let d = scratch(tag).join("dist");
fs::create_dir_all(d.join("assets")).unwrap();
fs::write(d.join("index.html"), b"<body>hi</body>").unwrap();
fs::write(d.join("app_bg.wasm"), b"\0asm\x01\0\0\0").unwrap();
fs::write(d.join("assets/app.css"), b".x{}").unwrap();
let blob = pack_dir(&d).unwrap();
(d, blob)
}
#[test]
fn unpack_artifact_round_trips_pack_dir() {
let (src, blob) = make_dist("rt-src");
let out = scratch("rt-out");
unpack_artifact(&blob, &out).unwrap();
assert_eq!(
fs::read(out.join("index.html")).unwrap(),
b"<body>hi</body>"
);
assert_eq!(
fs::read(out.join("app_bg.wasm")).unwrap(),
b"\0asm\x01\0\0\0"
);
assert_eq!(fs::read(out.join("assets/app.css")).unwrap(), b".x{}");
let _ = fs::remove_dir_all(src.parent().unwrap());
let _ = fs::remove_dir_all(&out);
}
#[test]
fn unpack_artifact_rejects_corruption_and_traversal() {
let out = scratch("bad-out");
assert_eq!(
unpack_artifact(b"not-a-blob", &out).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
let mut t = DIST_BLOB_HEADER.to_vec();
t.extend_from_slice(&[0, 0, 0]);
assert!(unpack_artifact(&t, &out).is_err());
let mut e = DIST_BLOB_HEADER.to_vec();
let rel = b"../escape.txt";
e.extend_from_slice(&(rel.len() as u64).to_be_bytes());
e.extend_from_slice(rel);
e.extend_from_slice(&(3u64).to_be_bytes());
e.extend_from_slice(b"pwn");
assert_eq!(
unpack_artifact(&e, &out).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert!(!out.parent().unwrap().join("escape.txt").exists());
let _ = fs::remove_dir_all(&out);
}
#[test]
fn materialize_latest_green_no_green_evicted_and_done() {
let project = scratch("mat-proj");
fs::create_dir_all(&project).unwrap();
let cache = scratch("mat-cache");
let out = scratch("mat-out");
assert_eq!(
materialize_latest_green(&LocalDiskStore::new(&cache), &project, &out).unwrap(),
Materialized::NoGreen
);
let (distdir, blob) = make_dist("mat-dist");
let orch =
BuildOrchestrator::new(LocalDiskStore::new(&cache), DistCompiler { blob }, &project);
assert_eq!(
orch.run(&BuildTrigger { identity: ident() }).outcome,
BuildOutcome::Compiled
);
match materialize_latest_green(&LocalDiskStore::new(&cache), &project, &out).unwrap() {
Materialized::Materialized(pa) => {
assert_eq!(pa.artifact.input_hash, input_hash(&ident()));
}
other => panic!("expected Materialized, got {other:?}"),
}
assert_eq!(
fs::read(out.join("index.html")).unwrap(),
b"<body>hi</body>"
);
assert_eq!(fs::read(out.join("assets/app.css")).unwrap(), b".x{}");
fs::remove_dir_all(&cache).unwrap();
match materialize_latest_green(&LocalDiskStore::new(&cache), &project, &out).unwrap() {
Materialized::Evicted(pa) => {
assert_eq!(pa.artifact.input_hash, input_hash(&ident()))
}
other => panic!("expected Evicted, got {other:?}"),
}
let _ = fs::remove_dir_all(&project);
let _ = fs::remove_dir_all(&out);
let _ = fs::remove_dir_all(distdir.parent().unwrap());
}
}