use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;
use flate2::read::GzDecoder;
use tempfile::TempDir;
fn cargo_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cairn"))
}
fn examples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("examples")
}
fn fallback_in_tempdir() -> (TempDir, PathBuf) {
let tmp = TempDir::new().expect("tempdir");
let src = examples_dir().join("edition-fallback.crn");
let dst = tmp.path().join("edition-fallback.crn");
fs::copy(&src, &dst).expect("copy edition-fallback");
(tmp, dst)
}
fn read_bytes(path: &Path) -> Vec<u8> {
fs::read(path).expect("read artifact")
}
fn gunzip(bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
GzDecoder::new(bytes)
.read_to_end(&mut out)
.expect("gzip decode");
out
}
fn bytes_contain(hay: &[u8], needle: &str) -> bool {
hay.windows(needle.len()).any(|w| w == needle.as_bytes())
}
#[test]
fn ac5_java_compile_writes_oak_sign_and_not_wall_sign() {
let (_tmp_src, src) = fallback_in_tempdir();
let out_dir = TempDir::new().expect("out tempdir");
let result = Command::new(cargo_bin())
.args([
"compile",
src.to_str().unwrap(),
"--edition",
"java",
"--out",
out_dir.path().to_str().unwrap(),
])
.output()
.expect("run cairn");
assert!(
result.status.success(),
"stderr={}",
String::from_utf8_lossy(&result.stderr),
);
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("W_INTENT_DEGRADED"),
"Java compile of a bare oak_sign should not degrade, got: {stderr}",
);
let nbt = out_dir.path().join("shop.nbt");
let bytes = gunzip(&read_bytes(&nbt));
assert!(
bytes_contain(&bytes, "minecraft:oak_sign"),
"Java palette must include oak_sign (from shop_java's floating_text slot)",
);
assert!(
!bytes_contain(&bytes, "minecraft:oak_wall_sign"),
"Java palette must not include the Bedrock variant's oak_wall_sign",
);
}
#[test]
fn ac6_bedrock_compile_writes_oak_wall_sign_and_not_oak_sign() {
let (_tmp_src, src) = fallback_in_tempdir();
let out_dir = TempDir::new().expect("out tempdir");
let result = Command::new(cargo_bin())
.args([
"compile",
src.to_str().unwrap(),
"--edition",
"bedrock",
"--out",
out_dir.path().to_str().unwrap(),
])
.output()
.expect("run cairn");
assert!(
result.status.success(),
"stderr={}",
String::from_utf8_lossy(&result.stderr),
);
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("W_INTENT_DEGRADED"),
"Bedrock compile of a bare oak_wall_sign should not degrade, got: {stderr}",
);
let structure = out_dir.path().join("shop.mcstructure");
let bytes = read_bytes(&structure);
let raw_oak_sign_hits = bytes
.windows(b"oak_sign".len())
.filter(|w| *w == b"oak_sign")
.count();
let wall_sign_hits = bytes
.windows(b"oak_wall_sign".len())
.filter(|w| *w == b"oak_wall_sign")
.count();
assert!(
wall_sign_hits > 0,
"Bedrock palette must include oak_wall_sign (from shop_bedrock's floating_text slot); got {wall_sign_hits} hits",
);
assert_eq!(
raw_oak_sign_hits, 0,
"Bedrock palette must not include the Java variant's plain oak_sign; got {raw_oak_sign_hits} bytewise hits",
);
}
#[test]
fn check_edition_flag_wires_strict_variant_pin_through_cli() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let path = tmp.path().join("bedrock_only_slot.crn");
std::fs::write(
&path,
[
"@cairn 2026.06",
"@requires version>=1.20",
"",
"theme t_java:",
" slot floor -> @oak_planks",
"",
"theme t_bedrock:",
" slot floor -> @oak_planks",
" slot bedrock_only -> @dark_oak_planks",
"",
"struct s size=4x4",
" floor mat_slot=bedrock_only",
"",
]
.join("\n"),
)
.expect("write test crn");
let path_str = path.to_str().unwrap();
let out = Command::new(cargo_bin())
.args(["check", "--edition", "bedrock", path_str])
.output()
.expect("run cairn");
assert!(
out.status.success(),
"check --edition bedrock must succeed; stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let out = Command::new(cargo_bin())
.args(["check", path_str])
.output()
.expect("run cairn");
assert!(
out.status.success(),
"check (no --edition) must succeed via sibling-slot union; stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let out = Command::new(cargo_bin())
.args(["check", "--edition", "java", path_str])
.output()
.expect("run cairn");
assert_eq!(
out.status.code(),
Some(1),
"check --edition java must exit 1 on Bedrock-only slot; stdout={}; stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("E_UNRESOLVED_SLOT"),
"expected E_UNRESOLVED_SLOT in diagnostics, got stdout={stdout}",
);
}
#[test]
fn ac7_check_exits_zero_across_all_three_edition_states() {
let src = examples_dir().join("edition-fallback.crn");
let src_str = src.to_str().unwrap();
let cases: [&[&str]; 3] = [
&["check", src_str],
&["check", "--edition", "java", src_str],
&["check", "--edition", "bedrock", src_str],
];
for args in cases {
let result = Command::new(cargo_bin())
.args(args)
.output()
.expect("run cairn");
assert!(
result.status.success(),
"check {args:?} must accept edition-fallback.crn; stderr={}",
String::from_utf8_lossy(&result.stderr),
);
}
}
#[test]
fn ac8_cottage_still_compiles_under_both_editions() {
let src = examples_dir().join("cottage.crn");
let tmp = TempDir::new().expect("tempdir");
let copied = tmp.path().join("cottage.crn");
fs::copy(&src, &copied).expect("copy cottage");
for edition in ["java", "bedrock"] {
let out_dir = TempDir::new().expect("out tempdir");
let result = Command::new(cargo_bin())
.args([
"compile",
copied.to_str().unwrap(),
"--edition",
edition,
"--out",
out_dir.path().to_str().unwrap(),
])
.output()
.expect("run cairn");
assert!(
result.status.success(),
"cottage compile under --edition {edition} must succeed; stderr={}",
String::from_utf8_lossy(&result.stderr),
);
}
}