use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
fn largo() -> Command {
let exe = std::env::var_os("CARGO_BIN_EXE_largo")
.unwrap_or_else(|| env!("CARGO_BIN_EXE_largo").into());
Command::new(exe)
}
fn node_available() -> bool {
Command::new("node").arg("--version").output().map(|o| o.status.success()).unwrap_or(false)
}
fn scaffold(name: &str, source: &str) -> TempDir {
let dir = TempDir::new().expect("temp dir");
std::fs::write(
dir.path().join("Largo.toml"),
format!("[package]\nname=\"{name}\"\nversion=\"0.1.0\"\nentry=\"src/main.lg\"\n"),
)
.unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/main.lg"), source).unwrap();
dir
}
fn run_node(mjs: &Path, args: &[&str]) -> String {
let out = Command::new("node").arg(mjs).args(args).output().expect("spawn node");
assert!(out.status.success(), "node failed running {}:\n{}", mjs.display(), String::from_utf8_lossy(&out.stderr));
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn build_emit_wasm(dir: &Path, name: &str) -> (PathBuf, PathBuf) {
let out = largo().current_dir(dir).args(["build", "--emit", "wasm"]).output().expect("spawn largo");
assert!(
out.status.success(),
"`largo build --emit wasm` failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let target = dir.join("target");
(target.join(format!("{name}.wasm")), target.join(format!("{name}.mjs")))
}
const PROGRAMS: &[(&str, &str)] = &[
("scalar", "## Main\nShow 6 times 7.\n"),
("floaty", "## Main\nShow 10.0 divided by 4.0.\n"),
("texty", "## Main\nShow \"hello\" + \" world\".\n"),
("negation", "## Main\nShow 3 minus 10.\n"),
("multi", "## Main\nShow 1 plus 1.\nShow 2 times 3.\n"),
];
#[test]
fn build_emit_wasm_writes_a_valid_module_and_shim() {
for (name, src) in PROGRAMS {
let dir = scaffold(name, src);
let (wasm, mjs) = build_emit_wasm(dir.path(), name);
assert!(wasm.exists(), "{name}.wasm was not written");
assert!(mjs.exists(), "{name}.mjs host shim was not written");
let bytes = std::fs::read(&wasm).unwrap();
assert!(bytes.len() > 8, "{name}.wasm is suspiciously small");
assert_eq!(&bytes[0..4], b"\0asm", "{name}.wasm lacks the wasm magic number");
assert_eq!(&bytes[4..8], &[1, 0, 0, 0], "{name}.wasm is not wasm version 1");
}
}
#[test]
fn build_emit_wasm_runs_byte_identically_to_the_vm() {
if !node_available() {
eprintln!("SKIP build_emit_wasm_runs_byte_identically_to_the_vm: node not installed");
return;
}
for (name, src) in PROGRAMS {
let dir = scaffold(name, src);
let (_wasm, mjs) = build_emit_wasm(dir.path(), name);
let got = run_node(&mjs, &[]);
let expected = logicaffeine_compile::compile::vm_outcome(src).output;
assert_eq!(
got.trim(),
expected.trim(),
"emitted wasm output disagrees with the VM for `{name}`:\n{src}"
);
}
}
#[test]
fn run_emit_wasm_compiles_and_runs_in_one_step() {
if !node_available() {
eprintln!("SKIP run_emit_wasm_compiles_and_runs_in_one_step: node not installed");
return;
}
let src = "## Main\nShow 40 plus 2.\n";
let dir = scaffold("runit", src);
let out = largo().current_dir(dir.path()).args(["run", "--emit", "wasm"]).output().expect("spawn largo");
assert!(
out.status.success(),
"`largo run --emit wasm` failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
logicaffeine_compile::compile::vm_outcome(src).output.trim(),
"`largo run --emit wasm` output disagrees with the VM"
);
}
fn is_toolchain_absence(stderr: &str) -> bool {
["toolchain", "wasm32", "cargo", "rustc", "lld", "base wasm32", "sysroot"].iter().any(|m| stderr.contains(m))
}
#[test]
fn build_emit_wasm_linked_computes_bigint_matching_the_vm() {
if !node_available() {
eprintln!("SKIP build_emit_wasm_linked_computes_bigint_matching_the_vm: node not installed");
return;
}
for (name, src) in [
("bigpow", "## Main\nShow 2 to the power of 200.\n"),
("bigmul", "## Main\nShow 99999999999 times 99999999999.\n"),
("bigchain", "## Main\nShow (2 to the power of 100) times (3 to the power of 50).\n"),
("bigmixed", "## Main\nShow \"result = \" + (2 to the power of 128).\n"),
] {
let dir = scaffold(name, src);
let out = largo().current_dir(dir.path()).args(["build", "--emit", "wasm-linked"]).output().expect("spawn largo");
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
if is_toolchain_absence(&stderr) {
eprintln!("SKIP build_emit_wasm_linked_...: toolchain / base wasm32 build unavailable:\n{stderr}");
return;
}
panic!("`largo build --emit wasm-linked` failed for `{name}`:\nstdout: {}\nstderr: {stderr}", String::from_utf8_lossy(&out.stdout));
}
let mjs = dir.path().join("target").join(format!("{name}.mjs"));
let got = run_node(&mjs, &[]);
let expected = logicaffeine_compile::compile::vm_outcome(src).output;
assert_eq!(
got.trim(),
expected.trim(),
"`--emit wasm-linked` (real BigInt) output disagrees with the VM for `{name}`:\n{src}"
);
}
}
#[test]
fn unknown_emit_target_is_rejected() {
let dir = scaffold("bad", "## Main\nShow 1.\n");
let out = largo().current_dir(dir.path()).args(["build", "--emit", "elf"]).output().expect("spawn largo");
assert!(!out.status.success(), "an unknown --emit target must fail, not silently succeed");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("elf") || stderr.contains("emit"), "the error should name the bad --emit target:\n{stderr}");
}