use std::collections::hash_map::DefaultHasher;
use std::ffi::OsString;
use std::hash::{Hash, Hasher};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;
use rayon::prelude::*;
use tishlang_native::compile_many_to_native;
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
}
fn core_dir() -> PathBuf {
workspace_root().join("tests").join("core")
}
fn expected_path(path: &Path) -> PathBuf {
path.with_file_name(format!(
"{}.expected",
path.file_name().unwrap().to_string_lossy()
))
}
fn get_expected(path: &Path) -> Option<String> {
let p = expected_path(path);
std::fs::read_to_string(&p).ok()
}
fn target_dir() -> PathBuf {
std::env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| workspace_root().join("target"))
}
fn integration_compile_cache_dir() -> PathBuf {
target_dir().join("integration_compile_cache")
}
fn native_build_features_for_integration_test() -> Vec<String> {
let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities()
.into_iter()
.collect();
v.sort();
v
}
fn combined_mvp_native_inputs_hash(paths: &[PathBuf]) -> u64 {
let mut h = DefaultHasher::new();
let feats = native_build_features_for_integration_test();
feats.len().hash(&mut h);
for f in &feats {
f.hash(&mut h);
}
paths.len().hash(&mut h);
for p in paths {
p.file_name()
.unwrap_or_default()
.to_string_lossy()
.hash(&mut h);
file_content_hash(p).hash(&mut h);
}
let codegen_rs = workspace_root().join("crates/tish_compile/src/codegen.rs");
if codegen_rs.is_file() {
file_content_hash(&codegen_rs).hash(&mut h);
}
let value_rs = workspace_root().join("crates/tish_core/src/value.rs");
if value_rs.is_file() {
file_content_hash(&value_rs).hash(&mut h);
}
let infer_rs = workspace_root().join("crates/tish_compile/src/infer.rs");
if infer_rs.is_file() {
file_content_hash(&infer_rs).hash(&mut h);
}
h.finish()
}
fn mvp_native_batch_cache_dir(combined: u64) -> PathBuf {
integration_compile_cache_dir()
.join("native_many")
.join(format!("{:016x}", combined))
}
struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: &str) -> Self {
let previous = std::env::var_os(key);
std::env::set_var(key, value);
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.previous {
None => std::env::remove_var(self.key),
Some(v) => std::env::set_var(self.key, v),
}
}
}
fn file_content_hash(path: &Path) -> u64 {
let mut f = std::fs::File::open(path).expect("open file for hash");
let mut content = Vec::new();
f.read_to_end(&mut content).expect("read file for hash");
let mut h = DefaultHasher::new();
path.to_string_lossy().hash(&mut h);
content.hash(&mut h);
h.finish()
}
fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
let stem = path.file_stem().unwrap().to_string_lossy();
let bin_stamp = std::fs::metadata(bin)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let hash = {
let mut h = DefaultHasher::new();
file_content_hash(path).hash(&mut h);
bin_stamp.hash(&mut h);
h.finish()
};
let hash8 = &format!("{:016x}", hash)[..8];
let cache_base = integration_compile_cache_dir().join(backend);
let _ = std::fs::create_dir_all(&cache_base);
let leaf = format!("{}__{}__{}", stem, backend, hash8);
let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
"native" => {
let ext = if cfg!(target_os = "windows") {
".exe"
} else {
""
};
let cached = cache_base.join(format!("{}{}", leaf, ext));
let args = vec![
OsString::from("build"),
OsString::from(path),
OsString::from("-o"),
OsString::from(&cached),
];
(cached, args)
}
"cranelift" => {
let ext = if cfg!(target_os = "windows") {
".exe"
} else {
""
};
let cached = cache_base.join(format!("{}{}", leaf, ext));
let args = vec![
OsString::from("build"),
OsString::from(path),
OsString::from("-o"),
OsString::from(&cached),
OsString::from("--native-backend"),
OsString::from("cranelift"),
];
(cached, args)
}
"js" => {
let cached = cache_base.join(format!("{}.js", leaf));
let args = vec![
OsString::from("build"),
OsString::from(path),
OsString::from("--target"),
OsString::from("js"),
OsString::from("-o"),
OsString::from(&cached),
];
(cached, args)
}
"wasi" => {
let out_base = cache_base.join(&leaf);
let artifact = out_base.with_extension("wasm");
let args = vec![
OsString::from("build"),
OsString::from(path),
OsString::from("-o"),
OsString::from(&out_base),
OsString::from("--target"),
OsString::from("wasi"),
];
(artifact, args)
}
_ => panic!("unknown backend {}", backend),
};
if !artifact_path.exists() {
let out = Command::new(bin)
.args(compile_args)
.current_dir(workspace_root())
.output()
.expect("run tish build");
assert!(
out.status.success(),
"Compile failed for {} ({}): {}",
path.display(),
backend,
String::from_utf8_lossy(&out.stderr)
);
}
let ext = artifact_path
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let temp_dest =
std::env::temp_dir().join(format!("tish_cached_{}_{}_{}", backend, stem, hash8));
let temp_dest = if ext.is_empty() {
temp_dest
} else {
temp_dest.with_extension(ext)
};
std::fs::copy(&artifact_path, &temp_dest).expect("copy cached artifact to temp");
temp_dest
}
fn tish_bin() -> PathBuf {
let bin_name = if cfg!(target_os = "windows") {
"tish.exe"
} else {
"tish"
};
let default = target_dir().join("debug").join(bin_name);
if default.exists() {
return default;
}
let llvm_cov = workspace_root()
.join("target")
.join("llvm-cov-target")
.join("debug")
.join(bin_name);
if llvm_cov.exists() {
return llvm_cov;
}
default
}
#[test]
fn test_tish_version_flag() {
let bin = tish_bin();
assert!(
bin.exists(),
"tish binary not found. Run `cargo build -p tishlang` first."
);
let out = Command::new(&bin).arg("-V").output().expect("run tish -V");
assert!(
out.status.success(),
"tish -V failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains(env!("CARGO_PKG_VERSION")),
"tish -V should print version {}; got: {}",
env!("CARGO_PKG_VERSION"),
stdout
);
let out2 = Command::new(&bin)
.arg("--version")
.output()
.expect("run tish --version");
assert!(out2.status.success());
let stdout2 = String::from_utf8_lossy(&out2.stdout);
assert!(
stdout2.contains(env!("CARGO_PKG_VERSION")),
"tish --version should print version"
);
}
#[test]
fn test_async_await_parse() {
let path = workspace_root()
.join("examples")
.join("async-await")
.join("src")
.join("main.tish");
if path.exists() {
let source = std::fs::read_to_string(&path).unwrap();
let result = tishlang_parser::parse(&source);
assert!(
result.is_ok(),
"Parse failed for {}: {:?}",
path.display(),
result.err()
);
}
}
#[test]
#[cfg(feature = "http")]
fn test_async_await_compile_via_binary() {
let bin = tish_bin();
let path = workspace_root()
.join("examples")
.join("async-await")
.join("src")
.join("main.tish");
if path.exists() && bin.exists() {
let out = std::env::temp_dir().join("tish_async_test_out");
let compile_result = Command::new(&bin)
.args([
"build",
path.to_string_lossy().as_ref(),
"-o",
out.to_string_lossy().as_ref(),
])
.current_dir(workspace_root())
.output();
let compile_out = compile_result.expect("run tish build");
assert!(
compile_out.status.success(),
"tish build failed: {}",
String::from_utf8_lossy(&compile_out.stderr)
);
let run_result = Command::new(&out).current_dir(workspace_root()).output();
let run_out = run_result.expect("run compiled async binary");
assert!(
run_out.status.success(),
"compiled async binary failed: {}",
String::from_utf8_lossy(&run_out.stderr)
);
let stdout = String::from_utf8_lossy(&run_out.stdout);
assert!(
stdout.contains("Fetching"),
"expected output to mention fetching"
);
assert!(stdout.contains("Done"), "expected output to contain Done");
}
}
#[test]
#[cfg(feature = "http")]
#[ignore = "timing and network sensitive; run manually: cargo test test_async_parallel_vs_sequential_timing -p tishlang--features http -- --ignored"]
fn test_async_parallel_vs_sequential_timing() {
let bin = tish_bin();
let parallel_src = workspace_root()
.join("examples")
.join("async-await")
.join("src")
.join("parallel.tish");
let sequential_src = workspace_root()
.join("examples")
.join("async-await")
.join("src")
.join("sequential.tish");
if !parallel_src.exists() || !sequential_src.exists() || !bin.exists() {
return;
}
let out_parallel = std::env::temp_dir().join("tish_parallel_timing");
let out_sequential = std::env::temp_dir().join("tish_sequential_timing");
let compile_par = Command::new(&bin)
.args([
"build",
parallel_src.to_string_lossy().as_ref(),
"-o",
out_parallel.to_string_lossy().as_ref(),
])
.current_dir(workspace_root())
.output();
assert!(
compile_par.as_ref().unwrap().status.success(),
"compile parallel: {}",
String::from_utf8_lossy(&compile_par.as_ref().unwrap().stderr)
);
let compile_seq = Command::new(&bin)
.args([
"build",
sequential_src.to_string_lossy().as_ref(),
"-o",
out_sequential.to_string_lossy().as_ref(),
])
.current_dir(workspace_root())
.output();
assert!(
compile_seq.as_ref().unwrap().status.success(),
"compile sequential: {}",
String::from_utf8_lossy(&compile_seq.as_ref().unwrap().stderr)
);
let t_parallel = std::time::Instant::now();
let run_par = Command::new(&out_parallel)
.current_dir(workspace_root())
.output();
let elapsed_parallel = t_parallel.elapsed();
assert!(
run_par.as_ref().unwrap().status.success(),
"run parallel: {}",
String::from_utf8_lossy(&run_par.as_ref().unwrap().stderr)
);
let t_sequential = std::time::Instant::now();
let run_seq = Command::new(&out_sequential)
.current_dir(workspace_root())
.output();
let elapsed_sequential = t_sequential.elapsed();
assert!(
run_seq.as_ref().unwrap().status.success(),
"run sequential: {}",
String::from_utf8_lossy(&run_seq.as_ref().unwrap().stderr)
);
let parallel_secs = elapsed_parallel.as_secs_f64();
let sequential_secs = elapsed_sequential.as_secs_f64();
assert!(
parallel_secs < sequential_secs * 0.6,
"Async NOT validated: parallel took {:.2}s but sequential took {:.2}s. Parallel must be < 60% of sequential to prove non-blocking.",
parallel_secs,
sequential_secs
);
}
#[test]
#[cfg(feature = "http")]
#[ignore = "requires async runtime; use test_async_await_compile_via_binary for CI"]
fn test_async_await_run() {
let path = workspace_root()
.join("examples")
.join("async-await")
.join("src")
.join("main.tish");
if path.exists() {
let source = std::fs::read_to_string(&path).unwrap();
let result = tishlang_eval::run(&source);
assert!(
result.is_ok(),
"Run failed for {}: {:?}",
path.display(),
result.err()
);
}
}
#[test]
fn test_promise_core() {
let bin = tish_bin();
if !bin.exists() {
return;
}
let path = workspace_root()
.join("tests")
.join("modules")
.join("promise.tish");
if !path.exists() {
return;
}
let expected = "\
new Promise: new-ctor
Promise sync resolve: 42
Promise.resolve: 100
Promise.reject caught: true
.then chain: 4
.catch: handled: fail
Promise.all: 1 2 3
Promise.race: fast
Promise.any: any-win
Promise.allSettled: fulfilled rejected reason
Promise tests completed
";
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
let mut args = backend_args.clone();
args.push(path.to_string_lossy().to_string().leak());
let out = Command::new(&bin)
.args(&args)
.current_dir(workspace_root())
.output()
.expect("run tish binary");
assert!(
out.status.success(),
"promise.tish ({:?}) failed: stderr={}",
backend_args,
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"Promise output mismatch on backend {:?} — check new Promise/any/allSettled regressions",
backend_args
);
}
}
#[test]
fn test_import_alias() {
let bin = tish_bin();
if !bin.exists() {
return;
}
let path = workspace_root()
.join("tests")
.join("modules")
.join("import_alias.tish");
if !path.exists() {
return;
}
let expected = "42\nhi there\n42\n1.0\n";
for backend_args in [vec!["run"], vec!["run", "--backend", "vm"]] {
let mut args = backend_args.clone();
args.push(path.to_string_lossy().to_string().leak());
let out = Command::new(&bin)
.args(&args)
.current_dir(workspace_root())
.output()
.expect("run tish binary");
assert!(
out.status.success(),
"import_alias.tish ({:?}) failed: stderr={}",
backend_args,
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"import alias output mismatch on backend {:?}",
backend_args
);
}
}
#[test]
fn test_module_private_binding_isolation() {
let bin = tish_bin();
if !bin.exists() {
return;
}
let path = workspace_root()
.join("tests")
.join("modules")
.join("private_isolation.tish");
if !path.exists() {
return;
}
let expected = "from-a:helper-a from-b:helper-b\narg inner\nhelper-b\n";
for backend in ["interp", "vm"] {
let out = Command::new(&bin)
.args(["run", "--backend", backend])
.arg(&path)
.current_dir(workspace_root())
.output()
.expect("run tish binary");
assert!(
out.status.success(),
"private_isolation.tish ({backend}) failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"module private-binding isolation mismatch on backend {backend}"
);
}
let native_bin = compile_cached(&bin, &path, "native");
let out = Command::new(&native_bin)
.current_dir(workspace_root())
.output()
.expect("run native binary");
let _ = std::fs::remove_file(&native_bin);
assert!(
out.status.success(),
"private_isolation native run failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"module private-binding isolation mismatch on native backend"
);
let node_available = Command::new("node")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if node_available {
let out_js = compile_cached(&bin, &path, "js");
let out = Command::new("node")
.arg(&out_js)
.current_dir(workspace_root())
.output()
.expect("run node");
let _ = std::fs::remove_file(&out_js);
assert!(
out.status.success(),
"private_isolation JS run failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"module private-binding isolation mismatch on JS target"
);
}
}
#[test]
#[cfg(feature = "http")]
#[ignore = "requires async runtime"]
fn test_async_promise_settimeout_combined() {
let path = workspace_root()
.join("tests")
.join("modules")
.join("async_promise_settimeout.tish");
if path.exists() {
let source = std::fs::read_to_string(&path).unwrap();
let result = tishlang_eval::run(&source);
assert!(
result.is_ok(),
"Failed to run async_promise_settimeout: {:?}",
result.err()
);
}
}
#[test]
fn test_vm_date_now() {
let path = workspace_root()
.join("tests")
.join("core")
.join("date.tish");
if !path.exists() {
return;
}
let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
tishlang_compile::detect_cycles(&modules).expect("cycles");
let program = tishlang_compile::merge_modules(modules)
.expect("merge")
.program;
let chunk = tishlang_bytecode::compile(&program).expect("compile");
let result = tishlang_vm::run(&chunk);
assert!(
result.is_ok(),
"VM run (library) failed: {:?}",
result.err()
);
let bin = tish_bin();
if bin.exists() {
let out = Command::new(&bin)
.args(["run", path.to_string_lossy().as_ref()])
.current_dir(workspace_root())
.output()
.expect("run tish binary");
assert!(
out.status.success(),
"tish run failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
}
#[test]
fn test_promise_combinators() {
let bin = tish_bin();
if !bin.exists() {
return;
}
let path = workspace_root()
.join("tests")
.join("modules")
.join("promise_combinators.tish");
if !path.exists() {
return;
}
let expected = "\
any first-fulfilled: win
any all-rejected: [\"e1\",\"e2\"]
allSettled[0] ok: 10
allSettled[1] rejected: boom
allSettled[2] ok: 30
race winner: A
any passthrough: 42
";
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
let mut args = backend_args.clone();
args.push(path.to_string_lossy().to_string().leak());
let out = Command::new(&bin)
.args(&args)
.current_dir(workspace_root())
.output()
.expect("run tish binary");
assert!(
out.status.success(),
"promise_combinators ({:?}) failed: stderr={}",
backend_args,
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"Promise.any/allSettled/race divergence on backend {:?}",
backend_args
);
}
}
#[test]
fn test_async_ordering_documented() {
let bin = tish_bin();
if !bin.exists() {
return;
}
let path = workspace_root()
.join("tests")
.join("modules")
.join("async_ordering.tish");
if !path.exists() {
return;
}
let expected = "\
1: sync-start
2: await = 42
2b: new Promise = ctor-ran
3: all = a b c
4: chain = 13
5: caught = boom
6: all-reject = rej
7: post-await = after-timer-was-queued
9: sync-end
8: timer-fires-LAST (await did not yield)
";
for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
let mut args = backend_args.clone();
args.push(path.to_string_lossy().to_string().leak());
let out = Command::new(&bin)
.args(&args)
.current_dir(workspace_root())
.output()
.expect("run tish binary");
assert!(
out.status.success(),
"async_ordering ({:?}) failed: stderr={}",
backend_args,
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"async ordering divergence on backend {:?} — the documented blocking-await/timer contract changed",
backend_args
);
}
}
#[test]
fn test_vm_index_assign_direct() {
let source = r#"let arr = [1, 2, 3]; arr[1] = 99; console.log(arr[1]);"#;
let program = tishlang_parser::parse(source).expect("parse");
let chunk = tishlang_bytecode::compile(&program).expect("compile");
let result = tishlang_vm::run(&chunk);
assert!(result.is_ok(), "VM IndexAssign failed: {:?}", result.err());
}
#[test]
fn test_vm_index_assign_via_resolve() {
let path = workspace_root()
.join("tests")
.join("core")
.join("array_sort_minimal.tish");
let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
tishlang_compile::detect_cycles(&modules).expect("cycles");
let program = tishlang_compile::merge_modules(modules)
.expect("merge")
.program;
let chunk = tishlang_bytecode::compile(&program).expect("compile");
let result = tishlang_vm::run(&chunk);
assert!(
result.is_ok(),
"VM IndexAssign via resolve failed: {:?}",
result.err()
);
}
#[test]
fn test_tish_run_index_assign() {
let bin = tish_bin();
let path = workspace_root()
.join("tests")
.join("core")
.join("array_sort_minimal.tish");
if !bin.exists() {
eprintln!("Skipping: tish binary not built");
return;
}
let out = Command::new(&bin)
.args(["run", path.to_string_lossy().as_ref()])
.current_dir(workspace_root())
.output()
.expect("run tish");
assert!(
out.status.success(),
"tish run failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
String::from_utf8_lossy(&out.stdout).contains("pass"),
"Expected 'pass' in output"
);
}
#[test]
fn test_full_stack_parse() {
let core_dir = core_dir();
for entry in std::fs::read_dir(&core_dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().map(|e| e == "tish").unwrap_or(false) {
let source = std::fs::read_to_string(&path).unwrap();
let result = tishlang_parser::parse(&source);
assert!(
result.is_ok(),
"Parse failed for {}: {:?}",
path.display(),
result.err()
);
}
}
}
const TIMING_NONDETERMINISTIC: &[&str] = &[
"array_stress.tish",
"array_stress_01_large_array_creation.tish",
"array_stress_02_iteration.tish",
"array_stress_03_map_filter_reduce.tish",
"array_stress_04_chained.tish",
"array_stress_05_sorting.tish",
"array_stress_06_search.tish",
"array_stress_07_splice_slice.tish",
"array_stress_08_concat_spread.tish",
"array_stress_09_flat.tish",
"array_stress_10_objects.tish",
"basic_types.tish",
"benchmark_granular.tish",
"new_features_perf.tish",
"object_stress.tish",
"objects_perf.tish",
"string_methods_perf.tish",
"recursion_stress.tish",
"jit_probe.tish",
];
const VM_PARITY_SKIP: &[&str] = &[];
fn discover_core_tests() -> Vec<String> {
let mut v: Vec<String> = std::fs::read_dir(core_dir())
.expect("read tests/core")
.filter_map(|e| {
let p = e.ok()?.path();
if p.extension().map(|x| x == "tish").unwrap_or(false) && expected_path(&p).exists() {
Some(p.file_name()?.to_string_lossy().into_owned())
} else {
None
}
})
.filter(|n| !TIMING_NONDETERMINISTIC.contains(&n.as_str()))
.collect();
v.sort();
v
}
fn has_js_sibling(name: &str) -> bool {
core_dir()
.join(name)
.with_extension("js")
.exists()
}
#[test]
fn checker_no_false_positives_on_corpus() {
let mut flagged: Vec<String> = Vec::new();
for name in discover_core_tests() {
let src = std::fs::read_to_string(core_dir().join(&name)).unwrap();
if let Ok(prog) = tishlang_parser::parse(&src) {
let diags = tishlang_compile::check_program(&prog);
if !diags.is_empty() {
let msgs: Vec<String> = diags.iter().map(|d| d.message.clone()).collect();
flagged.push(format!("{name}: {}", msgs.join(" | ")));
}
}
}
assert!(
flagged.is_empty(),
"type checker flagged valid corpus programs (false positives):\n{}",
flagged.join("\n")
);
}
#[test]
fn test_mvp_programs_interpreter() {
let core_dir = core_dir();
let bin = tish_bin();
assert!(
bin.exists(),
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
bin.display()
);
let regenerate = std::env::var("REGENERATE_EXPECTED").as_deref() == Ok("1");
for name in &discover_core_tests() {
let path = core_dir.join(name);
if !path.exists() {
continue;
}
let path_str = path.to_string_lossy();
let out = Command::new(&bin)
.args(["run", path_str.as_ref(), "--backend", "interp"])
.current_dir(workspace_root())
.output()
.expect("run tish interpreter");
assert!(
out.status.success(),
"Interpreter failed for {}: {}",
path.display(),
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
if regenerate {
std::fs::write(expected_path(&path), &stdout).expect("write expected");
} else {
let expected = get_expected(&path).unwrap_or_else(|| {
panic!(
"missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
path.display()
)
});
assert_eq!(
stdout,
expected,
"Interpreter output mismatch for {}",
path.display()
);
}
}
}
#[test]
fn test_mvp_programs_interp_vm_stdout_parity() {
let core_dir = core_dir();
let bin = tish_bin();
assert!(
bin.exists(),
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
bin.display()
);
for name in &discover_core_tests() {
if VM_PARITY_SKIP.contains(&name.as_str()) {
continue;
}
let path = core_dir.join(name);
if !path.exists() {
continue;
}
let path_str = path.to_string_lossy();
let out_interp = Command::new(&bin)
.args(["run", path_str.as_ref(), "--backend", "interp"])
.current_dir(workspace_root())
.output()
.expect("run tish interpreter");
assert!(
out_interp.status.success(),
"Interpreter failed for {}: {}",
path.display(),
String::from_utf8_lossy(&out_interp.stderr)
);
let out_vm = Command::new(&bin)
.args(["run", path_str.as_ref()])
.current_dir(workspace_root())
.output()
.expect("run tish VM");
assert!(
out_vm.status.success(),
"VM failed for {}: {}",
path.display(),
String::from_utf8_lossy(&out_vm.stderr)
);
let s_interp = String::from_utf8_lossy(&out_interp.stdout);
let s_vm = String::from_utf8_lossy(&out_vm.stdout);
assert_eq!(
s_interp,
s_vm,
"interp vs VM stdout mismatch for {}",
path.display()
);
}
}
#[test]
fn test_mvp_programs_native() {
let _fast_native = EnvVarGuard::set("TISH_FAST_NATIVE_BUILD", "1");
let core_dir = core_dir();
let bin = tish_bin();
assert!(
bin.exists(),
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
bin.display()
);
let mut paths: Vec<PathBuf> = discover_core_tests()
.iter()
.filter_map(|name| {
let p = core_dir.join(name);
if p.exists() {
Some(p)
} else {
None
}
})
.collect();
paths.sort();
if paths.is_empty() {
return;
}
let combined = combined_mvp_native_inputs_hash(&paths);
let cache_dir = mvp_native_batch_cache_dir(combined);
let _ = std::fs::create_dir_all(&cache_dir);
let ext = if cfg!(target_os = "windows") {
".exe"
} else {
""
};
let entries_owned: Vec<(PathBuf, PathBuf)> = paths
.iter()
.map(|p| {
let stem = p.file_stem().unwrap().to_string_lossy();
let cached = cache_dir.join(format!("{}{}", stem, ext));
(p.clone(), cached)
})
.collect();
let need_build = entries_owned.iter().any(|(_, o)| !o.exists());
if need_build {
let refs: Vec<(&Path, &Path)> = entries_owned
.iter()
.map(|(a, b)| (a.as_path(), b.as_path()))
.collect();
let feats = native_build_features_for_integration_test();
compile_many_to_native(&refs, Some(workspace_root().as_path()), &feats, true)
.unwrap_or_else(|e| panic!("compile_many_to_native: {}", e.message));
}
let errors: Vec<String> = entries_owned
.iter()
.enumerate()
.filter_map(|(run_index, (path, cached_bin))| {
let expected = match get_expected(path) {
Some(e) => e,
None => return Some(format!("missing expected: {}", path.display())),
};
if !cached_bin.exists() {
return Some(format!("missing cached binary: {}", cached_bin.display()));
}
let stem = path.file_stem().unwrap().to_string_lossy();
let ext_bin = cached_bin
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let temp_dest = std::env::temp_dir().join(format!(
"tish_mvp_native_{}_{:x}_{}_{}",
stem,
file_content_hash(path),
std::process::id(),
run_index
));
let temp_dest = if ext_bin.is_empty() {
temp_dest
} else {
temp_dest.with_extension(&ext_bin)
};
std::fs::copy(cached_bin, &temp_dest).expect("copy cached native bin to temp");
let out_bin = temp_dest;
let out = match Command::new(&out_bin)
.current_dir(workspace_root())
.output()
{
Ok(o) => o,
Err(e) => {
let _ = std::fs::remove_file(&out_bin);
return Some(format!("{}: run failed: {}", path.display(), e));
}
};
let _ = std::fs::remove_file(&out_bin);
if !out.status.success() {
return Some(format!(
"{}: {}",
path.display(),
String::from_utf8_lossy(&out.stderr)
));
}
let stdout = String::from_utf8_lossy(&out.stdout);
if stdout != expected {
return Some(format!("{}: output mismatch", path.display()));
}
None
})
.collect();
assert!(errors.is_empty(), "native failures:\n{}", errors.join("\n"));
}
#[test]
fn test_mvp_programs_cranelift() {
let core_dir = core_dir();
let bin = tish_bin();
assert!(
bin.exists(),
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
bin.display()
);
let errors: Vec<String> = discover_core_tests()
.par_iter()
.filter_map(|name| {
let path = core_dir.join(name);
if !path.exists() {
return None;
}
let expected = match get_expected(&path) {
Some(e) => e,
None => return Some(format!("missing expected: {}", path.display())),
};
let out_bin = compile_cached(&bin, &path, "cranelift");
let out = match Command::new(&out_bin)
.current_dir(workspace_root())
.output()
{
Ok(o) => o,
Err(e) => {
let _ = std::fs::remove_file(&out_bin);
return Some(format!("{}: run failed: {}", path.display(), e));
}
};
let _ = std::fs::remove_file(&out_bin);
if !out.status.success() {
return Some(format!(
"{}: {}",
path.display(),
String::from_utf8_lossy(&out.stderr)
));
}
let stdout = String::from_utf8_lossy(&out.stdout);
if stdout != expected {
return Some(format!("{}: output mismatch", path.display()));
}
None
})
.collect();
assert!(
errors.is_empty(),
"cranelift failures:\n{}",
errors.join("\n")
);
}
#[test]
fn test_mvp_programs_wasi() {
let wasmtime_available = Command::new("wasmtime")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !wasmtime_available {
eprintln!("Skipping test_mvp_programs_wasi: wasmtime not found");
return;
}
let core_dir = core_dir();
let bin = tish_bin();
assert!(
bin.exists(),
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
bin.display()
);
let errors: Vec<String> = discover_core_tests()
.par_iter()
.filter_map(|name| {
let path = core_dir.join(name);
if !path.exists() {
return None;
}
let expected = match get_expected(&path) {
Some(e) => e,
None => return Some(format!("missing expected: {}", path.display())),
};
let out_wasm = compile_cached(&bin, &path, "wasi");
let out = match Command::new("wasmtime")
.arg(out_wasm.as_os_str())
.current_dir(workspace_root())
.output()
{
Ok(o) => o,
Err(e) => {
let _ = std::fs::remove_file(&out_wasm);
return Some(format!("{}: wasmtime failed: {}", path.display(), e));
}
};
let _ = std::fs::remove_file(&out_wasm);
if !out.status.success() {
return Some(format!(
"{}: {}",
path.display(),
String::from_utf8_lossy(&out.stderr)
));
}
let stdout = String::from_utf8_lossy(&out.stdout);
if stdout != expected {
return Some(format!("{}: output mismatch", path.display()));
}
None
})
.collect();
assert!(errors.is_empty(), "wasi failures:\n{}", errors.join("\n"));
}
const JS_SKIP_FILES: &[&str] = &["typeof.tish", "void.tish"];
#[test]
fn test_mvp_programs_js() {
let node_available = Command::new("node")
.args(["--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !node_available {
eprintln!("Skipping test_mvp_programs_js: Node.js not found");
return;
}
let core_dir = core_dir();
let bin = tish_bin();
assert!(
bin.exists(),
"tish binary not found at {}. Run `cargo build -p tishlang` first.",
bin.display()
);
for name in &discover_core_tests() {
if JS_SKIP_FILES.contains(&name.as_str()) || !has_js_sibling(name) {
continue;
}
let path = core_dir.join(name);
if !path.exists() {
continue;
}
let expected = get_expected(&path).unwrap_or_else(|| {
panic!(
"missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
path.display()
)
});
let out_js = compile_cached(&bin, &path, "js");
let out = Command::new("node")
.arg(&out_js)
.current_dir(workspace_root())
.output()
.expect("run node");
let _ = std::fs::remove_file(&out_js);
assert!(
out.status.success(),
"Node failed for {}: {}",
path.display(),
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(
stdout,
expected,
"JS output mismatch for {}",
path.display()
);
}
}