use assert_cmd::Command;
use std::path::PathBuf;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("crates/nova-cli has a repo root two levels up")
.to_path_buf()
}
fn nova() -> Command {
Command::cargo_bin("nova").expect("nova binary builds")
}
#[test]
fn gate_1_hello_world_runs() {
nova()
.arg("run")
.arg(repo_root().join("examples/01-hello-world/src/main.nova"))
.assert()
.success()
.stdout("Hello, World!\n");
}
#[test]
fn gate_2_fibonacci_runs() {
nova()
.arg("run")
.arg(repo_root().join("examples/02-fibonacci/src/main.nova"))
.assert()
.success()
.stdout("fib(10) = 55\n");
}
#[test]
fn gate_2_fibonacci_reads_its_argument() {
nova()
.current_dir(repo_root().join("examples/02-fibonacci"))
.arg("run")
.arg("--")
.arg("20")
.assert()
.success()
.stdout("fib(20) = 6765\n");
}
#[test]
fn gate_3_match_on_enum_runs() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/match_enum.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/match_enum.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn gate_4_generic_functions_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/generics.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/generics.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn records_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/records.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/records.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn field_assign_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/field_assign.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/field_assign.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn field_assign_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/field_assign.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/field_assign.nova", "field_assign");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn for_loops_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/for_loops.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/for_loops.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn arrays_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/arrays.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/arrays.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn array_out_of_bounds_aborts() {
let dir = std::env::temp_dir().join("nova-oob");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("oob.nova");
std::fs::write(
&file,
"fn main() { let a = [1, 2, 3]\n println(\"${a[5]}\") }",
)
.expect("write");
let exe = dir.join(format!("oob{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe).assert().failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(stderr.contains("out of bounds"), "stderr: {stderr}");
}
#[test]
fn array_repeat_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/array_repeat.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/array_repeat.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn array_repeat_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/array_repeat.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/array_repeat.nova", "array_repeat");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn repeat_array_negative_length_aborts() {
let dir = std::env::temp_dir().join("nova-neg-len");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("neg_len.nova");
std::fs::write(
&file,
"fn main() { let n = 0 - 1\n let a = [7; n]\n println(\"${a.len()}\") }",
)
.expect("write");
let exe = dir.join(format!("neg_len{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe).assert().failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("array length must not be negative"),
"stderr: {stderr}"
);
}
#[test]
fn repeat_array_overlong_length_aborts_instead_of_segfaulting() {
let dir = std::env::temp_dir().join("nova-huge-len");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("huge_len.nova");
std::fs::write(
&file,
"fn main() { let n = 1 << 60\n let a = [7; n]\n println(\"${a.len()}\") }",
)
.expect("write");
let exe = dir.join(format!("huge_len{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe).assert().failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("array length is too large"),
"stderr: {stderr}"
);
let assert = nova().arg("run").arg(&file).assert().failure();
let jit_stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
jit_stderr.contains("array length is too large"),
"stderr: {jit_stderr}"
);
}
#[test]
fn undescribable_allocation_size_aborts_cleanly() {
let dir = std::env::temp_dir().join("nova-max-len");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("max_len.nova");
std::fs::write(
&file,
"fn main() { let n = (1 << 60) - 2\n let a = [7; n]\n println(\"${a.len()}\") }",
)
.expect("write");
let exe = dir.join(format!("max_len{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe).assert().failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: allocation of 9223372036854775800 bytes exceeds the maximum object size of 9223372036854775792 bytes"),
"stderr: {stderr}"
);
assert!(
!stderr.contains("panicked at") && !stderr.contains("non-unwinding"),
"expected no Rust panic, stderr: {stderr}"
);
let assert = nova().arg("run").arg(&file).assert().failure();
let jit_stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
jit_stderr.contains("exceeds the maximum object size"),
"stderr: {jit_stderr}"
);
assert!(
!jit_stderr.contains("panicked at"),
"expected no Rust panic, stderr: {jit_stderr}"
);
}
#[test]
fn constants_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/constants.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/constants.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn break_continue_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/break_continue.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/break_continue.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn closures_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/closures.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/closures.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn closures_build_standalone() {
let out = build_and_run("tests/runtime/closures.nova", "closures");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/closures.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn generic_impls_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/generic_impls.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/generic_impls.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn generic_impls_build_standalone() {
let out = build_and_run("tests/runtime/generic_impls.nova", "generic_impls");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/generic_impls.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn method_generics_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/method_generics.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/method_generics.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn method_generics_build_standalone() {
let out = build_and_run("tests/runtime/method_generics.nova", "method_generics");
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/method_generics.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn where_clauses_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/where_clauses.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/where_clauses.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn where_clauses_build_standalone() {
let out = build_and_run("tests/runtime/where_clauses.nova", "where_clauses");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/where_clauses.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn prelude_option_result_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/prelude_option_result.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/prelude_option_result.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn prelude_option_result_build_standalone() {
let out = build_and_run(
"tests/runtime/prelude_option_result.nova",
"prelude_option_result",
);
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/prelude_option_result.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
#[cfg_attr(
target_os = "linux",
ignore = "E0902 at JIT time on Linux; see issue #3"
)]
fn extern_ffi_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/extern_ffi.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/extern_ffi.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn extern_ffi_build_standalone() {
let out = build_and_run("tests/runtime/extern_ffi.nova", "extern_ffi");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/extern_ffi.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn extern_unresolvable_symbol_is_a_clean_error_not_a_panic() {
let dir = std::env::temp_dir().join("nova-extern-unresolvable");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("bad.nova");
std::fs::write(
&file,
"extern \"C\" { fn totally_bogus_symbol_xyz(x: Int) -> Int }\n\
fn main() { println(\"${totally_bogus_symbol_xyz(3)}\") }\n",
)
.expect("write");
let assert = nova().arg("run").arg(&file).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0902"), "stderr: {stderr}");
assert!(!stderr.contains("panicked"), "should not panic: {stderr}");
assert!(
!stderr.contains("compiler bug"),
"not a compiler bug: {stderr}"
);
}
#[test]
fn selfless_inherent_method_called_on_instance_is_e0014_not_an_ice() {
let dir = std::env::temp_dir().join("nova-selfless-inherent-call");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("selfless_call.nova");
std::fs::write(
&file,
"record P { v: Int }\n\
impl P { fn make() -> P { P { v: 7 } } }\n\
fn main() {\n\
let p = P { v: 0 }\n\
let q = p.make()\n\
println(\"${q.v}\")\n\
}\n",
)
.expect("write");
let assert = nova().arg("run").arg(&file).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0014"), "stderr: {stderr}");
assert!(!stderr.contains("panicked"), "should not panic: {stderr}");
assert!(
!stderr.contains("compiler bug"),
"not a compiler bug: {stderr}"
);
}
#[test]
fn assoc_fn_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/assoc_fn.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/assoc_fn.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn assoc_fn_build_standalone() {
let out = build_and_run("tests/runtime/assoc_fn.nova", "assoc_fn");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/assoc_fn.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn trait_assoc_fn_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/trait_assoc_fn.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/trait_assoc_fn.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn trait_assoc_fn_build_standalone() {
let out = build_and_run("tests/runtime/trait_assoc_fn.nova", "trait_assoc_fn");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/trait_assoc_fn.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn supertraits_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/supertraits.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/supertraits.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn supertraits_build_standalone() {
let out = build_and_run("tests/runtime/supertraits.nova", "supertraits");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/supertraits.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn generic_trait_methods_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/generic_trait_methods.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/generic_trait_methods.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn generic_trait_methods_build_standalone() {
let out = build_and_run(
"tests/runtime/generic_trait_methods.nova",
"generic_trait_methods",
);
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/generic_trait_methods.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn conflicting_cross_module_extern_signatures_report_e0075() {
let dir = std::env::temp_dir().join("nova-extern-conflict");
std::fs::create_dir_all(&dir).expect("temp dir");
std::fs::write(
dir.join("mod_a.nova"),
"extern \"C\" { fn sqrt(x: Float) -> Float }\npub fn ca() -> Float { sqrt(16.0) }\n",
)
.expect("write a");
std::fs::write(
dir.join("mod_b.nova"),
"extern \"C\" { fn sqrt(x: Int) -> Int }\npub fn cb() -> Int { sqrt(16) }\n",
)
.expect("write b");
let main = dir.join("main.nova");
std::fs::write(
&main,
"import mod_a::{ca}\nimport mod_b::{cb}\nfn main() { println(\"${ca()} ${cb()}\") }\n",
)
.expect("write main");
let assert = nova().arg("check").arg(&main).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0075"), "stderr: {stderr}");
assert!(!stderr.contains("panicked"), "should not panic: {stderr}");
}
#[test]
fn modules_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/modules/main.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/modules/main.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn modules_build_standalone() {
let out = build_and_run("tests/runtime/modules/main.nova", "modules_main");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/modules/main.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn import_of_private_item_is_rejected() {
let dir = std::env::temp_dir().join("nova-mod-priv");
std::fs::create_dir_all(&dir).expect("temp dir");
std::fs::write(dir.join("lib.nova"), "fn hidden() -> Int { 1 }\n").expect("write lib");
let main = dir.join("app.nova");
std::fs::write(
&main,
"import lib::{hidden}\nfn main() { let x = hidden() }\n",
)
.expect("write");
let assert = nova().arg("check").arg(&main).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0001"), "stderr: {stderr}");
}
#[test]
fn modules_same_name_functions_dispatch_correctly() {
let dir = std::env::temp_dir().join("nova-mod-collide-fn");
std::fs::create_dir_all(&dir).expect("temp dir");
std::fs::write(
dir.join("lib.nova"),
"fn helper() -> Int { 200 }\npub fn lib_val() -> Int { helper() }\n",
)
.expect("write lib");
let main = dir.join("main.nova");
std::fs::write(
&main,
"import lib::{lib_val}\n\
fn helper() -> Int { 100 }\n\
fn main() {\n\
println(\"main=${helper()}\")\n\
println(\"lib=${lib_val()}\")\n\
}\n",
)
.expect("write main");
nova()
.arg("run")
.arg(&main)
.assert()
.success()
.stdout("main=100\nlib=200\n");
}
#[test]
fn modules_same_name_methods_dispatch_correctly() {
let dir = std::env::temp_dir().join("nova-mod-collide-method");
std::fs::create_dir_all(&dir).expect("temp dir");
std::fs::write(
dir.join("lib.nova"),
"record Box { v: Int }\n\
impl Box { fn get(self) -> Int { self.v + 1000 } }\n\
pub fn make_b() -> Int { Box { v: 7 }.get() }\n",
)
.expect("write lib");
let main = dir.join("main.nova");
std::fs::write(
&main,
"import lib::{make_b}\n\
record Box { v: Int }\n\
impl Box { fn get(self) -> Int { self.v } }\n\
fn main() {\n\
let a = Box { v: 1 }\n\
println(\"main=${a.get()}\")\n\
println(\"lib=${make_b()}\")\n\
}\n",
)
.expect("write main");
nova()
.arg("run")
.arg(&main)
.assert()
.success()
.stdout("main=1\nlib=1007\n");
}
#[test]
fn multi_segment_import_is_rejected() {
let dir = std::env::temp_dir().join("nova-mod-qualified");
std::fs::create_dir_all(&dir).expect("temp dir");
std::fs::write(dir.join("foo.nova"), "pub fn marker() -> Int { 1 }\n").expect("write foo");
std::fs::write(dir.join("bar.nova"), "pub fn marker() -> Int { 2 }\n").expect("write bar");
let main = dir.join("main.nova");
std::fs::write(
&main,
"import foo::bar\nfn main() { println(\"${marker()}\") }\n",
)
.expect("write main");
let assert = nova().arg("check").arg(&main).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0900"), "stderr: {stderr}");
}
#[test]
fn traits_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/traits.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/traits.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn traits_build_standalone() {
let out = build_and_run("tests/runtime/traits.nova", "traits");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/traits.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn check_reports_type_errors_with_code() {
let dir = std::env::temp_dir().join("nova-check-test");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("bad.nova");
std::fs::write(&file, "fn main() { let x: Int = \"hello\" }").expect("write test file");
let assert = nova().arg("check").arg(&file).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0010"), "stderr was: {stderr}");
}
#[test]
fn check_passes_on_valid_program() {
nova()
.arg("check")
.arg(repo_root().join("examples/02-fibonacci/src/main.nova"))
.assert()
.success();
}
#[test]
fn check_rejects_unsatisfied_trait_bound() {
let dir = std::env::temp_dir().join("nova-check-bound");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("bound.nova");
std::fs::write(
&file,
"record P { v: Int }\n\
trait Show { fn name(self) -> String }\n\
impl Show for P { fn name(self) -> String { \"p\" } }\n\
fn label<T: Show>(x: T) -> String { x.name() }\n\
fn main() { println(label(5)) }\n",
)
.expect("write");
let assert = nova().arg("check").arg(&file).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0013"), "stderr: {stderr}");
}
#[test]
fn check_rejects_impl_missing_a_supertrait() {
let dir = std::env::temp_dir().join("nova-check-supertrait");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("supertrait.nova");
std::fs::write(
&file,
"trait Named { fn name(self) -> String }\n\
trait Ranked: Named { fn rank(self) -> Int }\n\
record P { v: Int }\n\
impl Ranked for P { fn rank(self) -> Int { self.v } }\n\
fn main() { }\n",
)
.expect("write");
let assert = nova().arg("check").arg(&file).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0072"), "stderr: {stderr}");
assert!(
stderr.contains("requires `Named`"),
"the diagnostic should name the missing supertrait: {stderr}"
);
}
#[test]
fn run_and_check_accept_and_run_an_await_free_async_fn_at_float() {
let dir = std::env::temp_dir().join("nova-async-no-await-float");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("float_no_await.nova");
std::fs::write(
&file,
"async fn f() -> Float { 1.5 }\n\
fn main() { let x = f()\n println(\"done\") }\n",
)
.expect("write test file");
nova().arg("check").arg(&file).assert().success();
let run_assert = nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout("done\n");
let run_stderr = String::from_utf8_lossy(&run_assert.get_output().stderr).to_string();
assert!(
!run_stderr.contains("E0088"),
"an await-free async fn must no longer be rejected: {run_stderr}"
);
assert!(
!run_stderr.contains("internal codegen error"),
"must not reach the codegen ICE path: {run_stderr}"
);
assert!(
!run_stderr.contains("this is a compiler bug"),
"must not reach the codegen ICE path: {run_stderr}"
);
}
#[test]
fn run_accepts_an_await_free_async_fn_at_int() {
let dir = std::env::temp_dir().join("nova-async-no-await-int");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("int_no_await.nova");
std::fs::write(
&file,
"async fn f() -> Int { 1 }\n\
fn main() { let x = f()\n println(\"done\") }\n",
)
.expect("write test file");
nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout("done\n");
}
#[test]
fn run_and_check_accept_and_run_an_async_fn_containing_await() {
let dir = std::env::temp_dir().join("nova-async-with-await");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("with_await.nova");
std::fs::write(
&file,
"async fn f() -> Float { 1.5 }\n\
async fn g() -> Float { f().await }\n\
fn main() { let x = g()\n println(\"done\") }\n",
)
.expect("write test file");
nova().arg("check").arg(&file).assert().success();
let run_assert = nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout("done\n");
let run_stderr = String::from_utf8_lossy(&run_assert.get_output().stderr).to_string();
assert!(
!run_stderr.contains("E0088"),
"an `async fn` containing `.await` must no longer be rejected: {run_stderr}"
);
assert!(
!run_stderr.contains("internal codegen error"),
"must not reach the codegen ICE path: {run_stderr}"
);
assert!(
!run_stderr.contains("this is a compiler bug"),
"must not reach the codegen ICE path: {run_stderr}"
);
}
#[test]
fn run_accepts_an_await_inside_a_loop() {
let dir = std::env::temp_dir().join("nova-async-await-loop");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("await_loop.nova");
std::fs::write(
&file,
"async fn one() -> Float { 1.0 }\n\
async fn total(n: Int) -> Float {\n\
\x20 let mut i = 0\n\
\x20 let mut t = 0.0\n\
\x20 while i < n { t = t + one().await\n i = i + 1 }\n\
\x20 t\n\
}\n\
fn main() { let x = total(3)\n println(\"done\") }\n",
)
.expect("write test file");
nova().arg("check").arg(&file).assert().success();
nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout("done\n");
}
#[test]
fn run_drives_an_async_main() {
let dir = std::env::temp_dir().join("nova-async-main");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("async_main.nova");
std::fs::write(
&file,
"async fn step(n: Int) -> Int {\n\
\x20 yield_now().await\n\
\x20 n + 1\n\
}\n\
async fn main() {\n\
\x20 println(\"start\")\n\
\x20 println(\"got ${step(41).await}\")\n\
}\n",
)
.expect("write test file");
nova().arg("check").arg(&file).assert().success();
nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout("start\ngot 42\n");
}
#[test]
fn gate_async_tasks_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/async_tasks.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/async_tasks.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn gate_async_tasks_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/async_tasks.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/async_tasks.nova", "async_tasks");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn gate_async_tasks_under_gc_stress() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/async_tasks.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/async_tasks.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn interpolation_nary_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/interpolation_nary.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/interpolation_nary.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn interpolation_nary_under_gc_stress() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/interpolation_nary.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/interpolation_nary.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn a_spawned_tasks_string_output_survives_release_at_completion() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/spawned_output_survives_release.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/spawned_output_survives_release.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn gate_task_sleep_order_runs() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/task_sleep_order.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/task_sleep_order.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn gate_producer_consumer_channel_runs() {
nova()
.arg("run")
.arg(repo_root().join("examples/03-producer-consumer/src/main.nova"))
.assert()
.success()
.stdout("got 1\ngot 2\ngot 3\ngot 4\ngot 5\ndone\n");
}
#[test]
fn run_aborts_when_an_async_fn_calls_block_on() {
let dir = std::env::temp_dir().join("nova-nested-block-on");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("nested_block_on.nova");
std::fs::write(
&file,
"async fn inner() -> Int { 1 }\n\
async fn outer() -> Int { block_on(inner()) }\n\
fn main() { println(\"total ${block_on(outer())}\") }\n",
)
.expect("write test file");
nova().arg("check").arg(&file).assert().success();
let assert = nova().arg("run").arg(&file).assert().failure().stdout("");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("re-entrantly"),
"the abort must say what was violated: {stderr}"
);
assert!(
stderr.contains("nova: panic:"),
"the diagnostic must come from the runtime's abort path: {stderr}"
);
assert!(
!stderr.contains("panicked at"),
"the diagnostic must not unwind: an unwind here would have to pass \
through a generated poll frame, which has no unwind description at \
all: {stderr}"
);
}
#[test]
fn forged_join_handle_aborts_instead_of_hanging() {
let file = repo_root().join("tests/runtime/forged_join_handle.nova");
nova().arg("check").arg(&file).assert().success();
let assert = nova().arg("run").arg(&file).assert().failure().stdout("");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains(
"nova_rt_task_join_future: this future was never spawned, so there is no task to ask about"
),
"the abort must name the unspawned-future contract it violated: {stderr}"
);
assert!(
stderr.contains("nova: panic:"),
"the diagnostic must come from the runtime's abort path: {stderr}"
);
assert!(
!stderr.contains("panicked at"),
"the diagnostic must not unwind -- an unwind here would have to pass \
through a generated poll frame, which has no unwind description at \
all: {stderr}"
);
}
#[test]
fn a_recycled_state_address_does_not_resolve_a_never_spawned_future() {
let file = repo_root().join("tests/runtime/recycled_task_state.nova");
nova().arg("check").arg(&file).assert().success();
let assert = nova()
.arg("run")
.arg(&file)
.env("NOVA_GC_STRESS", "1")
.assert()
.failure()
.stdout("");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains(
"nova_rt_task_join_future: this future was never spawned, so there is no task to ask about"
),
"a future the executor never saw must be rejected however its state \
address was obtained: {stderr}"
);
assert!(
stderr.contains("nova: panic:"),
"the diagnostic must come from the runtime's abort path: {stderr}"
);
assert!(
!stderr.contains("panicked at"),
"the diagnostic must not unwind -- an unwind here would have to pass \
through a generated poll frame, which has no unwind description at \
all: {stderr}"
);
}
#[test]
fn task_deadlock_reports_and_aborts_instead_of_hanging_forever() {
let file = repo_root().join("tests/runtime/task_deadlock.nova");
nova().arg("check").arg(&file).assert().success();
let assert = nova().arg("run").arg(&file).assert().failure().stdout("");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: deadlock:"),
"the diagnostic must be the deadlock report, not a different abort: {stderr}"
);
assert!(
stderr.contains("is waiting for task"),
"the report must name what the parked task is waiting for: {stderr}"
);
}
#[test]
fn run_succeeds_when_async_fn_is_never_called() {
let dir = std::env::temp_dir().join("nova-async-unreached");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("unreached.nova");
std::fs::write(
&file,
"async fn f() -> Float { 1.5 }\nfn main() { println(\"fine\") }\n",
)
.expect("write test file");
nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout("fine\n");
}
fn build_and_run(source: &str, exe_name: &str) -> String {
let dir = std::env::temp_dir().join(format!("nova-build-tests-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
let exe = dir.join(format!("{exe_name}{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(repo_root().join(source))
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe)
.assert()
.success()
.get_output()
.stdout
.clone();
let _ = std::fs::remove_file(&exe);
String::from_utf8(out).expect("program output is UTF-8")
}
fn gc_live_object_counts(stderr: &str) -> Vec<u64> {
stderr
.lines()
.filter(|line| line.starts_with("nova-gc: collection"))
.filter_map(|line| {
let rest = &line[line.find(" bytes, ")? + " bytes, ".len()..];
rest.split(' ').next()?.parse().ok()
})
.collect()
}
#[test]
fn gc_reclaims_garbage() {
let dir = std::env::temp_dir().join("nova-gc-reclaim");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("garbage.nova");
std::fs::write(
&file,
"record Node { v: Int }\n\
fn main() {\n\
let mut i = 0\n\
while i < 300000 {\n\
let n = Node { v: i }\n\
i = i + 1\n\
}\n\
println(\"done ${i}\")\n\
}",
)
.expect("write");
let assert = nova()
.arg("run")
.arg(&file)
.env("NOVA_GC_DEBUG", "1")
.assert()
.success()
.stdout("done 300000\n");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova-gc: collection"),
"expected at least one collection: {stderr}"
);
let max_live = gc_live_object_counts(&stderr)
.into_iter()
.max()
.unwrap_or(0);
assert!(
max_live > 0 && max_live < 1000,
"live set should stay bounded, saw {max_live} live objects:\n{stderr}"
);
}
#[test]
fn release_without_toolchain_emits_ir_and_errors() {
let dir = std::env::temp_dir().join("nova-release-notool");
std::fs::create_dir_all(&dir).expect("temp dir");
let exe = dir.join(format!("hello{}", std::env::consts::EXE_SUFFIX));
let ll = exe.with_file_name(format!(
"{}.nova.ll",
exe.file_name().unwrap().to_string_lossy()
));
let _ = std::fs::remove_file(&ll);
let assert = nova()
.arg("build")
.arg("--release")
.arg(repo_root().join("examples/01-hello-world/src/main.nova"))
.arg("-o")
.arg(&exe)
.env("NOVA_CLANG", "nova_no_such_tool_xyz")
.env("NOVA_LLC", "nova_no_such_tool_xyz")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("LLVM toolchain"), "stderr: {stderr}");
assert!(ll.exists(), "expected generated IR at {}", ll.display());
let ir = std::fs::read_to_string(&ll).expect("read ir");
assert!(ir.contains("define i32 @main"), "ir:\n{ir}");
let _ = std::fs::remove_file(&ll);
}
#[test]
fn release_intermediate_never_aliases_output() {
let dir = std::env::temp_dir().join("nova-release-alias");
std::fs::create_dir_all(&dir).expect("temp dir");
let out = dir.join("prog.ll"); let ir = dir.join("prog.ll.nova.ll"); let _ = std::fs::remove_file(&out);
let _ = std::fs::remove_file(&ir);
nova()
.arg("build")
.arg("--release")
.arg(repo_root().join("examples/01-hello-world/src/main.nova"))
.arg("-o")
.arg(&out)
.env("NOVA_CLANG", "nova_no_such_tool_xyz")
.env("NOVA_LLC", "nova_no_such_tool_xyz")
.assert()
.failure();
assert!(ir.exists(), "expected IR at {}", ir.display());
let _ = std::fs::remove_file(&ir);
let _ = std::fs::remove_file(&out);
}
#[test]
fn release_builds_and_runs_when_clang_available() {
let clang_ok = std::process::Command::new("clang")
.arg("--version")
.output()
.is_ok();
if !clang_ok {
eprintln!("skipping: no clang on PATH");
return;
}
let dir = std::env::temp_dir().join("nova-release-run");
std::fs::create_dir_all(&dir).expect("temp dir");
let exe = dir.join(format!("hello_rel{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg("--release")
.arg(repo_root().join("examples/01-hello-world/src/main.nova"))
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe)
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(String::from_utf8(out).expect("utf8"), "Hello, World!\n");
let _ = std::fs::remove_file(&exe);
}
#[test]
fn build_hello_world_standalone() {
let out = build_and_run("examples/01-hello-world/src/main.nova", "hello");
assert_eq!(out, "Hello, World!\n");
}
#[test]
fn build_match_enum_standalone() {
let out = build_and_run("tests/runtime/match_enum.nova", "match_enum");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/match_enum.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn build_generics_standalone() {
let out = build_and_run("tests/runtime/generics.nova", "generics");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/generics.stdout"))
.expect("fixture")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn std_core_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/std_core.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/std_core.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn std_core_build_standalone() {
let out = build_and_run("tests/runtime/std_core.nova", "std_core");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/std_core.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
assert_eq!(out, expected);
}
#[test]
fn std_core_under_gc_stress() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/std_core.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/std_core.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn hash_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/hash.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/hash.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn hash_build_standalone() {
let out = build_and_run("tests/runtime/hash.nova", "hash");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/hash.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn collections_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/collections.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/collections.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn collections_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/collections.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/collections.nova", "collections");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn collections_under_gc_stress() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/collections.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/collections.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn record_literal_inside_an_interpolation_runs() {
let dir = std::env::temp_dir().join("nova-interp-record");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("interp_record.nova");
std::fs::write(
&file,
"record R { v: Int }\n\
record G { w: R }\n\
trait Display { fn fmt(self) -> String }\n\
impl Display for R { fn fmt(self) -> String { \"R(${self.v})\" } }\n\
fn f(r: R) -> Int { r.v }\n\
fn main() {\n\
println(\"${f(R { v: 1 })}\")\n\
println(\"${R { v: 2 }}\")\n\
println(\"${G { w: R { v: 3 } }.w.v}\")\n\
println(\"${if true { R { v: 4 }.v } else { 0 }}\")\n\
if \"${f(R { v: 5 })}\" == \"5\" { println(\"cond\") }\n\
}\n",
)
.expect("write");
let expected = "1\nR(2)\n3\n4\ncond\n";
nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout(expected);
let exe = dir.join(format!("interp_record{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
Command::new(&exe).assert().success().stdout(expected);
let _ = std::fs::remove_file(&exe);
}
#[test]
fn unterminated_interpolation_hole_is_a_clean_error() {
let dir = std::env::temp_dir().join("nova-interp-unterminated");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("unterminated.nova");
std::fs::write(
&file,
"record R { v: Int }\n\
fn f(r: R) -> Int { r.v }\n\
fn main() { println(\"${f(R { v: 1 }\") }\n",
)
.expect("write");
let assert = nova().arg("check").arg(&file).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("unterminated string interpolation"),
"stderr: {stderr}"
);
assert!(!stderr.contains("panicked"), "should not panic: {stderr}");
assert!(
!stderr.contains("compiler bug"),
"not a compiler bug: {stderr}"
);
}
#[test]
fn panic_aborts_with_message() {
let assert = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/panic_unwrap.nova"))
.assert()
.failure();
let out = assert.get_output();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stdout.contains('7'), "stdout was {stdout:?}");
assert!(
stderr.contains("nova: panic: called get on None"),
"stderr was {stderr:?}"
);
assert!(!stdout.contains("unreachable"), "stdout was {stdout:?}");
}
#[test]
fn std_strings_module_is_loaded() {
let dir = std::env::temp_dir().join("nova-strings-scaffold");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(
&path,
"fn main() { println(\"${\"\".is_empty()} ${\"x\".is_empty()}\") }",
)
.expect("write test file");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("true false\n");
}
#[test]
fn string_len_counts_codepoints_not_bytes() {
let src = "fn main() { println(\"${\"café\".len()} ${\"日本語\".len()} ${\"\".len()}\") }";
let dir = std::env::temp_dir().join("nova-strings-len");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("4 3 0\n");
}
#[test]
fn str_chars_array_matches_codegen_layout() {
let src = "fn main() {\n\
let cs = \"a→🦀\".chars()\n\
println(\"${cs.len()} ${cs[0]} ${cs[1]} ${cs[2]}\")\n\
let e = \"\".chars()\n\
println(\"${e.len()}\")\n\
println(\"${\"héllo\".char_at(0).unwrap_or('?')} \
${\"héllo\".char_at(1).unwrap_or('?')} \
${\"héllo\".char_at(5).unwrap_or('?')} \
${\"héllo\".char_at(9).unwrap_or('?')} \
${\"héllo\".char_at(0 - 1).unwrap_or('?')}\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-chars");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("3 a → 🦀\n0\nh é ? ? ?\n");
}
#[test]
fn str_from_chars_round_trips_and_slice_is_half_open() {
let src = "fn main() {\n\
println(\"${\"a→🦀é\".chars().len()}\")\n\
println(\"${\"héllo wörld\".slice(0, 5)}|${\"héllo\".slice(0, 0)}|\
${\"héllo\".slice(5, 5)}|${\"héllo\".slice(0, 5)}|\
${\"héllo wörld\".slice(6, 11)}\")\n\
println(\"${\"a→🦀\".reverse()} ${\"\".reverse()}\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-slice");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("4\nhéllo|||héllo|wörld\n🦀→a \n");
}
#[test]
fn string_slice_negative_start_panics() {
let dir = std::env::temp_dir().join("nova-strings-slice-neg-start");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(
&path,
"fn main() { println(\"${\"abc\".slice(0 - 1, 2)}\") }\n",
)
.expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: String::slice start is negative"),
"stderr: {stderr}"
);
}
#[test]
fn string_slice_end_past_len_panics() {
let dir = std::env::temp_dir().join("nova-strings-slice-end-oob");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, "fn main() { println(\"${\"abc\".slice(0, 4)}\") }\n").expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: String::slice end is past the end of the string"),
"stderr: {stderr}"
);
}
#[test]
fn string_slice_start_after_end_panics() {
let dir = std::env::temp_dir().join("nova-strings-slice-start-after-end");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, "fn main() { println(\"${\"abc\".slice(2, 1)}\") }\n").expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: String::slice start is after end"),
"stderr: {stderr}"
);
}
#[test]
fn string_search_is_codepoint_indexed_and_empty_needle_matches() {
let src = "fn main() {\n\
let s = \"héllo wörld\"\n\
println(\"${s.index_of(\"wörld\").unwrap_or(0 - 1)} \
${s.index_of(\"zzz\").unwrap_or(0 - 1)} \
${s.index_of(\"\").unwrap_or(0 - 1)}\")\n\
println(\"${s.starts_with(\"hé\")} ${s.starts_with(\"x\")} \
${s.ends_with(\"rld\")} ${s.ends_with(\"x\")}\")\n\
println(\"${s.contains(\"ö\")} ${s.contains(\"q\")} \
${s.contains(\"\")} ${s.starts_with(\"\")} ${s.ends_with(\"\")}\")\n\
println(\"${\"\".index_of(\"a\").unwrap_or(0 - 1)} \
${\"aaa\".index_of(\"aa\").unwrap_or(0 - 1)} \
${\"abc\".starts_with(\"abcd\")}\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-search");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("6 -1 0\ntrue false true false\ntrue false true true true\n-1 0 false\n");
}
#[test]
fn string_search_same_length_and_last_position_boundaries() {
let src = "fn main() {\n\
println(\"${\"abc\".ends_with(\"abc\")} ${\"ab\".ends_with(\"abc\")}\")\n\
println(\"${\"abc\".index_of(\"abc\").unwrap_or(0 - 1)} \
${\"ab\".index_of(\"abc\").unwrap_or(0 - 1)} \
${\"abcde\".index_of(\"de\").unwrap_or(0 - 1)}\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-search-boundary");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("true false\n0 -1 3\n");
}
#[test]
fn string_split_and_join_match_the_pinned_semantics() {
let src = "fn main() {\n\
let a = \"a,b,c\".split(\",\")\n\
println(\"${a.len()} ${a[0]}${a[1]}${a[2]}\")\n\
let b = \"abc\".split(\",\")\n\
println(\"${b.len()} ${b[0]}\")\n\
let c = \",a,\".split(\",\")\n\
println(\"${c.len()} [${c[0]}][${c[1]}][${c[2]}]\")\n\
let d = \"a,,b\".split(\",\")\n\
println(\"${d.len()} [${d[0]}][${d[1]}][${d[2]}]\")\n\
let e = \"a→b\".split(\"→\")\n\
println(\"${e.len()} ${e[0]}${e[1]}\")\n\
let f = \"abc\".split(\"\")\n\
println(\"${f.len()} ${f[0]}|${f[1]}|${f[2]}\")\n\
println(\"${\"\".split(\"\").len()} ${\"\".split(\",\").len()}\")\n\
let g = \"xx\".split(\"xx\")\n\
println(\"${g.len()} [${g[0]}][${g[1]}]\")\n\
println(\"[${\",\".join(a)}] [${\"\".join(f)}] [${\"-\".join([])}]\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-split");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova().arg("run").arg(&path).assert().success().stdout(
"3 abc\n1 abc\n3 [][a][]\n3 [a][][b]\n2 ab\n3 a|b|c\n0 1\n2 [][]\n\
[a,b,c] [abc] []\n",
);
}
#[test]
fn string_split_and_join_cover_multi_codepoint_separators_and_one_part_join() {
let src = "fn main() {\n\
let s = \"a::b::c\".split(\"::\")\n\
println(\"${s.len()} ${s[0]}|${s[1]}|${s[2]}\")\n\
let t = \"a→b→c\".split(\"→\")\n\
println(\"${t.len()} ${t[0]}|${t[1]}|${t[2]}\")\n\
let u = \"aaaa\".split(\"aa\")\n\
println(\"${u.len()} [${u[0]}][${u[1]}][${u[2]}]\")\n\
let v = \"ababab\".split(\"abab\")\n\
println(\"${v.len()} [${v[0]}][${v[1]}]\")\n\
let parts = [\"a\", \"b\", \"c\"]\n\
println(\"${\"->\".join(parts)}\")\n\
let one = [\"solo\"]\n\
println(\"${\",\".join(one)}\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-split-join-boundary");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("3 a|b|c\n3 a|b|c\n3 [][][]\n2 [][ab]\na->b->c\nsolo\n");
}
#[test]
fn string_trim_family_and_repeat() {
let src = "fn main() {\n\
let s = \" héllo\\t\\n\"\n\
println(\"[${s.trim()}][${s.trim_start()}][${s.trim_end()}]\")\n\
println(\"[${\" \".trim()}][${\"\".trim()}][${\"x\".trim()}]\")\n\
println(\"[${\"ab\".repeat(3)}][${\"ab\".repeat(0)}][${\"\".repeat(5)}]\")\n\
println(\"[${\"→\".repeat(2)}]\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-trim");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("[héllo][héllo\t\n][ héllo]\n[][][x]\n[ababab][][]\n[→→]\n");
}
#[test]
fn string_repeat_negative_count_panics() {
let dir = std::env::temp_dir().join("nova-strings-repeat-negative");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, "fn main() { println(\"${\"x\".repeat(0 - 1)}\") }\n").expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: String::repeat count must not be negative"),
"stderr: {stderr}"
);
}
#[test]
fn string_trim_covers_non_ascii_whitespace() {
let src = "fn main() {\n\
println(\"[${\"\\u{00A0}\\u{2002}\\u{2003}\\u{3000}mid\\u{3000}\\u{2003}\\u{2002}\\u{00A0}\".trim()}]\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-trim-unicode-ws");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("[mid]\n");
}
#[test]
fn string_trim_start_and_trim_end_pinned_on_all_whitespace() {
let src = "fn main() {\n\
println(\"[${\" \".trim_start()}][${\" \".trim_end()}]\")\n\
println(\"[${\"\\t\\t\".trim_start()}][${\"\\t\\t\".trim_end()}]\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-trim-start-end-all-ws");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("[][]\n[][]\n");
}
#[test]
fn string_case_mapping_is_whole_string_not_per_char() {
let src = "fn main() {\n\
println(\"${\"Straße\".to_upper()} ${\"ß\".to_upper().len()}\")\n\
println(\"${\"HÉLLO WÖRLD\".to_lower()} ${\"\".to_upper()}|\")\n\
println(\"${\"İ\".to_lower().len()} ${\"abc123\".to_upper()}\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-case");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("STRASSE 2\nhéllo wörld |\n2 ABC123\n");
}
#[test]
fn debug_for_string_escapes_into_a_valid_literal() {
let src = "fn main() {\n\
println(\"${(\"a\\\"b\").dbg()}\")\n\
println(\"${(\"back\\\\slash\").dbg()}\")\n\
println(\"${(\"tab\\there\").dbg()}\")\n\
println(\"${(\"\").dbg()} ${(\"é→\").dbg()}\")\n\
}";
let dir = std::env::temp_dir().join("nova-strings-dbg");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("\"a\\\"b\"\n\"back\\\\slash\"\n\"tab\\there\"\n\"\" \"é→\"\n");
}
#[test]
fn debug_for_string_escapes_every_control_arm_in_isolation() {
let src = r#"fn main() {
println("${("\\").dbg()}")
println("${("\n").dbg()}")
println("${("\t").dbg()}")
println("${("\r").dbg()}")
println("${("\0").dbg()}")
println("${("\"").dbg()}")
println("${("\\\n\t\r\0\"").dbg()}")
}
"#;
let dir = std::env::temp_dir().join("nova-strings-dbg-arms");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova().arg("run").arg(&path).assert().success().stdout(
r#""\\"
"\n"
"\t"
"\r"
"\0"
"\""
"\\\n\t\r\0\""
"#,
);
}
#[test]
fn debug_for_char_and_string_escape_only_their_own_quote() {
let src = r#"fn main() {
println("${('"').dbg()}")
println("${("'").dbg()}")
}
"#;
let dir = std::env::temp_dir().join("nova-strings-dbg-quote-cross");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova().arg("run").arg(&path).assert().success().stdout(
r#"'"'
"'"
"#,
);
}
#[test]
fn strings_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/strings.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/strings.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn strings_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/strings.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/strings.nova", "strings");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn strings_under_gc_stress() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/strings.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/strings.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn vec_set_out_of_range_aborts_with_message() {
let dir = std::env::temp_dir().join("nova-collections-setoob");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(
&path,
"fn main() {\n\
let mut v: Vec<Int> = Vec::new()\n\
v.push(1)\n\
v.set(5, 9)\n\
}",
)
.expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: Vec::set index out of range"),
"stderr: {stderr}"
);
}
#[test]
fn vec_set_negative_index_aborts_with_message() {
let dir = std::env::temp_dir().join("nova-collections-setoob-neg");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(
&path,
"fn main() {\n\
let mut v: Vec<Int> = Vec::new()\n\
v.push(1)\n\
v.set(0 - 1, 9)\n\
}",
)
.expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: Vec::set index out of range"),
"stderr: {stderr}"
);
}
#[test]
fn unwrap_on_the_wrong_variant_aborts_with_message() {
let dir = std::env::temp_dir().join("nova-collections-unwrap");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(
&path,
"fn main() {\n\
let o: Option<Int> = None\n\
println(\"${o.unwrap()}\")\n\
}",
)
.expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: called `unwrap` on a `None` value"),
"stderr: {stderr}"
);
}
#[test]
fn a_projection_resolves_per_instantiation_at_monomorphization() {
let src = "trait It { type Item\n fn get_item(self) -> Self::Item }\n\
record W<T> { v: T }\n\
impl<T> It for W<T> { type Item = T\n fn get_item(self) -> T { self.v } }\n\
fn unwrap_item<I: It>(x: I) -> I::Item { x.get_item() }\n\
fn main() {\n\
let a = unwrap_item(W { v: 7 })\n\
let b = unwrap_item(W { v: true })\n\
println(\"${a} ${b}\")\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-mono");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("7 true\n");
}
#[test]
fn a_projection_parameter_on_a_pinned_generic_reaches_codegen_resolved() {
let src = "trait It { type Item\n fn g(self) -> Int }\n\
record W { v: Int }\n\
impl It for W { type Item = Int\n fn g(self) -> Int { 1 } }\n\
fn f<I: It>(x: I, y: I::Item) -> Int { 7 }\n\
fn main() { println(\"${f(W { v: 1 }, 5)}\") }";
let dir = std::env::temp_dir().join("nova-assoc-mono-param");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
let assert = nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("7\n");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
!stderr.contains("verifier"),
"the projection must not reach codegen: {stderr}"
);
}
#[test]
fn a_projection_in_a_nested_calls_type_arguments_is_normalized_too() {
let src = "trait It { type Item\n fn get_item(self) -> Self::Item }\n\
record W<T> { v: T }\n\
impl<T> It for W<T> { type Item = T\n fn get_item(self) -> T { self.v } }\n\
fn id<T>(x: T) -> T { x }\n\
fn f<I: It>(x: I) -> I::Item { id(x.get_item()) }\n\
fn main() {\n\
let a = f(W { v: 7 })\n\
let b = f(W { v: true })\n\
println(\"${a} ${b}\")\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-mono-typeargs");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("7 true\n");
}
#[test]
fn a_vec_iterates_to_exhaustion_through_the_iterator_trait() {
let src = "fn main() {\n\
let mut v: Vec<Int> = Vec::new()\n\
v.push(10)\n\
v.push(20)\n\
v.push(40)\n\
let mut it = v.iter()\n\
let mut total = 0\n\
let mut nones = 0\n\
let mut n = 0\n\
while n < 6 {\n\
match it.next() {\n\
Some(x) => total = total + x,\n\
None => nones = nones + 1,\n\
}\n\
n = n + 1\n\
}\n\
println(\"total=${total} nones=${nones}\")\n\
let mut it2 = v.iter()\n\
let o: Option<Int> = it2.next()\n\
println(\"first=${o.unwrap_or(0)}\")\n\
let e: Vec<Int> = Vec::new()\n\
let mut ei = e.iter()\n\
match ei.next() {\n\
Some(x) => println(\"unexpected ${x}\"),\n\
None => println(\"empty ok\"),\n\
}\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-veciter");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("total=70 nones=3\nfirst=10\nempty ok\n");
}
#[test]
fn an_exhausted_vec_iterator_does_not_advance_its_cursor() {
let src = "fn main() {\n\
let mut v: Vec<Int> = Vec::new()\n\
v.push(10)\n\
let mut it = v.iter()\n\
match it.next() { Some(x) => println(\"a=${x}\"), None => println(\"a=none\") }\n\
match it.next() { Some(x) => println(\"b=${x}\"), None => println(\"b=none\") }\n\
v.push(20)\n\
match it.next() { Some(x) => println(\"c=${x}\"), None => println(\"c=none\") }\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-veciter-cursor");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("a=10\nb=none\nc=20\n");
}
#[test]
fn a_generic_function_over_iterator_resolves_item_per_instantiation() {
let src = "fn first<I: Iterator>(mut it: I) -> Option<I::Item> { it.next() }\n\
fn first_or<I: Iterator>(mut it: I, dflt: I::Item) -> I::Item {\n\
match it.next() { Some(x) => x, None => dflt }\n\
}\n\
fn main() {\n\
let mut ns: Vec<Int> = Vec::new()\n\
ns.push(7)\n\
let mut ss: Vec<String> = Vec::new()\n\
ss.push(\"hi\")\n\
match first(ns.iter()) { Some(n) => println(\"int=${n}\"), None => println(\"int=none\") }\n\
match first(ss.iter()) { Some(s) => println(\"str=${s}\"), None => println(\"str=none\") }\n\
let e: Vec<Bool> = Vec::new()\n\
match first(e.iter()) { Some(b) => println(\"bool=${b}\"), None => println(\"bool=none\") }\n\
println(\"or_int=${first_or(ns.iter(), 0)}\")\n\
println(\"or_str=${first_or(ss.iter(), \"?\")}\")\n\
println(\"or_empty=${first_or(e.iter(), true)}\")\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-iterator-generic");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
let assert = nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("int=7\nstr=hi\nbool=none\nor_int=7\nor_str=hi\nor_empty=true\n");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
!stderr.contains("verifier") && !stderr.contains("panicked"),
"no projection may reach codegen unresolved: {stderr}"
);
}
#[test]
fn a_vec_iterator_keeps_its_backing_storage_alive_under_gc_stress() {
let src = "fn make() -> VecIter<String> {\n\
let mut v: Vec<String> = Vec::new()\n\
v.push(\"a\")\n\
v.push(\"b\")\n\
v.push(\"c\")\n\
v.push(\"d\")\n\
v.push(\"e\")\n\
v.iter()\n\
}\n\
fn main() {\n\
let mut it = make()\n\
let mut out = \"\"\n\
let mut n = 0\n\
while n < 7 {\n\
match it.next() {\n\
Some(s) => out = \"${out}${s}\",\n\
None => out = \"${out}.\",\n\
}\n\
n = n + 1\n\
}\n\
println(\"out=${out}\")\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-veciter-gc");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("out=abcde..\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("out=abcde..\n");
}
#[test]
fn a_user_record_can_implement_the_std_iterator_trait() {
let src = "record Counter { n: Int }\n\
impl Iterator for Counter {\n\
type Item = Int\n\
fn next(mut self) -> Option<Self::Item> {\n\
if self.n >= 3 { return None }\n\
let x = self.n\n\
self.n = self.n + 1\n\
Some(x)\n\
}\n\
}\n\
fn first<I: Iterator>(mut it: I) -> Option<I::Item> { it.next() }\n\
fn main() {\n\
let mut c = Counter { n: 0 }\n\
match c.next() { Some(x) => println(\"a=${x}\"), None => println(\"a=none\") }\n\
let c0 = Counter { n: 0 }\n\
match first(c0) { Some(x) => println(\"b=${x}\"), None => println(\"b=none\") }\n\
let c9 = Counter { n: 9 }\n\
match first(c9) { Some(x) => println(\"c=${x}\"), None => println(\"c=none\") }\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-iterator-user-impl");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("a=0\nb=0\nc=none\n");
}
#[test]
fn calling_next_on_an_immutable_vec_iterator_reports_e0060() {
let src = "fn main() {\n\
let mut v: Vec<Int> = Vec::new()\n\
v.push(1)\n\
let it = v.iter()\n\
match it.next() { Some(x) => println(\"${x}\"), None => println(\"none\") }\n\
}";
let dir = std::env::temp_dir().join("nova-assoc-iterator-immutable");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
let assert = nova().arg("check").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("error[E0060]") && stderr.contains("`next` mutates its receiver"),
"stderr: {stderr}"
);
}
#[test]
fn assoc_types_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/assoc_types.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/assoc_types.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn assoc_types_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/assoc_types.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/assoc_types.nova", "assoc_types");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn assoc_types_under_gc_stress() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/assoc_types.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/assoc_types.nova"))
.assert()
.success()
.stdout(expected);
}
fn assoc_chain_in_a_generic_body(n: u32, link: impl Fn(u32) -> String) -> String {
let decls: String = (0..=n).map(|k| format!(" type A{k}\n")).collect();
let binds: String = (0..n)
.map(|k| format!(" type A{} = {}\n", k, link(k)))
.collect();
format!(
"record Pair<A, B> {{ a: A\n b: B }}\n\
trait Chain {{\n{decls} fn g(self) -> Int }}\n\
record W {{ v: Int }}\n\
impl Chain for W {{\n{binds} type A{n} = Int\n\
fn g(self) -> Int {{ 1 }} }}\n\
fn use_it<I: Chain>(x: I) -> Int {{\n\
let a: [I::A0] = []\n\
a.len() }}\n\
fn main() {{ println(\"${{use_it(W {{ v: 1 }})}}\") }}\n"
)
}
#[test]
fn mono_reports_which_normalization_limit_a_projection_overflowed() {
let dir = std::env::temp_dir().join("nova-mono-normalize-limits");
std::fs::create_dir_all(&dir).expect("temp dir");
let wide = dir.join("wide.nova");
std::fs::write(
&wide,
assoc_chain_in_a_generic_body(16, |k| format!("Pair<Self::A{}, Self::A{}>", k + 1, k + 1)),
)
.expect("write");
let assert = nova().arg("check").arg(&wide).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert_eq!(
stderr.matches("error[E0078]").count(),
1,
"one overflow, one report: {stderr}"
);
assert!(
stderr.contains("resolves to more than 10000 type nodes"),
"a wide chain is a size report, not a depth report: {stderr}"
);
assert!(
stderr.contains("`[W::A0]`") && stderr.contains("when instantiating `use_it`"),
"the message must name both the type and the instance: {stderr}"
);
let deep = dir.join("deep.nova");
std::fs::write(
&deep,
assoc_chain_in_a_generic_body(65, |k| format!("Self::A{}", k + 1)),
)
.expect("write");
let assert = nova().arg("check").arg(&deep).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert_eq!(
stderr.matches("error[E0078]").count(),
1,
"one overflow, one report: {stderr}"
);
assert!(
stderr.contains("the chain of bindings is more than 64 deep"),
"a deep chain is a depth report, not a size report: {stderr}"
);
}
#[test]
fn a_trait_bound_needs_an_impl_that_fits_structurally_not_just_by_head() {
let program = |b: &str| {
format!(
"trait It {{ type Item\n\
fn g(self) -> Int }}\n\
record P<A, B> {{ a: A\n\
b: B }}\n\
impl It for P<Int, Int> {{ type Item = Int\n\
fn g(self) -> Int {{ 1 }} }}\n\
fn f<I: It>(x: I) -> Int {{ 7 }}\n\
fn main() {{ println(\"${{f(P {{ a: 1, b: {b} }})}}\") }}\n"
)
};
let dir = std::env::temp_dir().join("nova-impl-structural-fit");
std::fs::create_dir_all(&dir).expect("temp dir");
let bad = dir.join("bad.nova");
std::fs::write(&bad, program("true")).expect("write");
let assert = nova().arg("run").arg(&bad).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("error[E0013]") && stderr.contains("`P: It`"),
"stderr: {stderr}"
);
let good = dir.join("good.nova");
std::fs::write(&good, program("2")).expect("write");
nova()
.arg("run")
.arg(&good)
.assert()
.success()
.stdout("7\n");
}
#[test]
fn iterator_adapters_chain_and_are_lazy() {
let src = "fn main() {\n\
let mut v = Vec::new()\n\
v.push(1)\n\
v.push(2)\n\
v.push(3)\n\
let mut m = MapIter { it: v.iter(), f: |n| n * 10 }\n\
for x in m { println(\"${x}\") }\n\
let mut f = FilterIter { it: v.iter(), keep: |n| n > 1 }\n\
for x in f { println(\"keep ${x}\") }\n\
let mut c = MapIter { it: FilterIter { it: v.iter(), keep: |n| n > 1 }, f: |n| n > 2 }\n\
for b in c { println(\"chain ${b}\") }\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-adapters");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("10\n20\n30\nkeep 2\nkeep 3\nchain false\nchain true\n");
}
#[test]
fn a_record_field_read_resolves_a_projection_through_a_now_concrete_parameter() {
let src = "trait It { type Item\n\
fn next(mut self) -> Option<Self::Item> }\n\
record M<I: It, U> { it: I, f: fn(I::Item) -> U }\n\
record Cache<I: It> { it: I, hit: I::Item }\n\
record Counter { n: Int }\n\
impl It for Counter {\n\
type Item = Int\n\
fn next(mut self) -> Option<Int> { None }\n\
}\n\
fn main() {\n\
let m = M { it: Counter { n: 0 }, f: |x| x + 1 }\n\
let g = m.f\n\
println(\"${g(5)}\")\n\
let c = Cache { it: Counter { n: 0 }, hit: 7 }\n\
let x = c.hit\n\
println(\"${x + 1}\")\n\
}";
let dir = std::env::temp_dir().join("nova-record-field-read-projection");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("6\n8\n");
}
#[test]
fn a_map_iter_calls_f_lazily_not_all_up_front() {
let src = "fn main() {\n\
let mut v = Vec::new()\n\
v.push(1)\n\
v.push(2)\n\
v.push(3)\n\
let mut m = MapIter { it: v.iter(), f: |n| { println(\"call\") n * 10 } }\n\
for x in m { println(\"got ${x}\") break }\n\
println(\"done\")\n\
}";
let dir = std::env::temp_dir().join("nova-map-iter-laziness");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("call\ngot 10\ndone\n");
}
#[test]
fn iterator_default_methods_work_and_chain() {
let src = "fn main() {\n\
let mut v = Vec::new()\n\
v.push(1)\n\
v.push(2)\n\
v.push(3)\n\
let got = v.iter().filter(|n| n > 1).map(|n| n * 10).collect()\n\
println(\"${got.len()}\")\n\
println(\"${got.get(0).unwrap()}\")\n\
println(\"${v.iter().fold(0, |a, x| a + x)}\")\n\
println(\"${v.iter().count()}\")\n\
println(\"${v.iter().any(|n| n > 2)}\")\n\
println(\"${v.iter().any(|n| n > 9)}\")\n\
println(\"${v.iter().map(|n| n > 2).any(|b| b)}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-default-methods");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("2\n20\n6\n3\ntrue\nfalse\ntrue\n");
}
#[test]
fn iterator_map_default_method_is_lazy_over_a_side_effecting_source() {
let src = "record PrintSrc { n: Int, max: Int }\n\
impl Iterator for PrintSrc {\n\
type Item = Int\n\
fn next(mut self) -> Option<Int> {\n\
if self.n >= self.max { return None }\n\
println(\"pull ${self.n}\")\n\
let x = self.n\n\
self.n = self.n + 1\n\
Some(x)\n\
}\n\
}\n\
fn main() {\n\
let src = PrintSrc { n: 0, max: 3 }\n\
let mut m = src.map(|x| x * 10)\n\
println(\"built\")\n\
println(\"${m.next().unwrap()}\")\n\
println(\"${m.next().unwrap()}\")\n\
println(\"${m.next().unwrap()}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-map-default-is-lazy");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("built\npull 0\n0\npull 1\n10\npull 2\n20\n");
}
#[test]
fn iterator_any_short_circuits_and_does_not_scan_past_the_first_match() {
let src = "record CountingSrc { n: Int, max: Int }\n\
impl Iterator for CountingSrc {\n\
type Item = Int\n\
fn next(mut self) -> Option<Int> {\n\
if self.n >= self.max { return None }\n\
println(\"pull ${self.n}\")\n\
let x = self.n\n\
self.n = self.n + 1\n\
Some(x)\n\
}\n\
}\n\
fn main() {\n\
let src = CountingSrc { n: 0, max: 5 }\n\
println(\"${src.any(|n| n > 1)}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-any-short-circuits");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("pull 0\npull 1\npull 2\ntrue\n");
}
#[test]
fn iterator_default_methods_work_through_a_generic_iterator_bound() {
let src = "fn count_via_fold<I: Iterator>(it: I) -> Int {\n\
it.fold(0, |a, x| a + 1)\n\
}\n\
fn any_via_generic<I: Iterator>(it: I) -> Bool {\n\
it.any(|x| true)\n\
}\n\
fn len_via_collect<I: Iterator>(it: I) -> Int {\n\
it.collect().len()\n\
}\n\
fn main() {\n\
let mut v = Vec::new()\n\
v.push(1)\n\
v.push(2)\n\
v.push(3)\n\
println(\"${count_via_fold(v.iter())}\")\n\
println(\"${any_via_generic(v.iter())}\")\n\
println(\"${len_via_collect(v.iter())}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-default-methods-generic-bound");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("3\ntrue\n3\n");
}
#[test]
fn a_generic_iterator_bound_cannot_use_its_item_concretely_in_fold() {
let src = "fn sum_via_fold<I: Iterator>(it: I) -> Int {\n\
it.fold(0, |a, x| a + x)\n\
}\n\
fn main() {}";
let dir = std::env::temp_dir().join("nova-iterator-generic-item-not-concrete");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
let assert = nova().arg("check").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("error[E0010]") && stderr.contains("T0::Item"),
"stderr: {stderr}"
);
}
#[test]
fn iterator_adapter_chain_compiles_and_runs_as_a_single_expression() {
let src = "fn main() {\n\
let mut v = Vec::new()\n\
v.push(1)\n\
v.push(2)\n\
v.push(3)\n\
let got = v.iter().filter(|n| n > 1).map(|n| n * 10).collect()\n\
println(\"${got.len()}\")\n\
println(\"${got.get(0).unwrap()}\")\n\
let bools = v.iter().map(|n| n > 2).collect()\n\
println(\"${bools.len()}\")\n\
println(\"${bools.get(2).unwrap()}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-chain-single-expression");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("2\n20\n3\ntrue\n");
}
#[test]
fn an_iterators_own_storage_still_advances_when_a_consumer_takes_plain_self() {
let src = "record Cursor { n: Int, max: Int }\n\
impl Iterator for Cursor {\n\
type Item = Int\n\
fn next(mut self) -> Option<Int> {\n\
if self.n >= self.max { return None }\n\
let x = self.n\n\
self.n = self.n + 1\n\
Some(x)\n\
}\n\
}\n\
fn main() {\n\
let mut a = Cursor { n: 0, max: 5 }\n\
println(\"${a.any(|x| x > 1)}\")\n\
println(\"${a.n}\")\n\
let mut b = Cursor { n: 0, max: 3 }\n\
println(\"${b.count()}\")\n\
println(\"${b.n}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-consumer-aliases-storage");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("true\n3\n3\n3\n");
}
#[test]
fn continue_and_break_work_inside_a_for_loop_over_an_iterator() {
let src = "fn main() {\n\
let mut v = Vec::new()\n\
v.push(1)\n\
v.push(2)\n\
v.push(3)\n\
v.push(4)\n\
v.push(5)\n\
let mut total = 0\n\
let mut seen = 0\n\
for x in v.iter() {\n\
seen = seen + 1\n\
if x == 2 { continue }\n\
if x == 4 { break }\n\
total = total + x\n\
}\n\
println(\"total=${total} seen=${seen}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-for-continue");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("total=4 seen=4\n");
}
#[test]
fn a_for_loop_over_a_generic_iterator_bound_binds_its_item_at_each_instantiation() {
let src = "record BoolSrc { n: Int }\n\
impl Iterator for BoolSrc {\n\
type Item = Bool\n\
fn next(mut self) -> Option<Bool> {\n\
if self.n >= 3 { return None }\n\
self.n = self.n + 1\n\
Some(self.n == 1)\n\
}\n\
}\n\
record FloatSrc { n: Int }\n\
impl Iterator for FloatSrc {\n\
type Item = Float\n\
fn next(mut self) -> Option<Float> {\n\
if self.n >= 2 { return None }\n\
self.n = self.n + 1\n\
Some(2.5)\n\
}\n\
}\n\
fn first_via_for<I: Iterator>(it: I) -> Option<I::Item> {\n\
for x in it { return Some(x) }\n\
None\n\
}\n\
fn count_via_for<I: Iterator>(it: I) -> Int {\n\
let mut n = 0\n\
for x in it { n = n + 1 }\n\
n\n\
}\n\
fn main() {\n\
let b = first_via_for(BoolSrc { n: 0 })\n\
if b.unwrap() { println(\"bool=true\") } else { println(\"bool=false\") }\n\
let f = first_via_for(FloatSrc { n: 0 })\n\
println(\"float=${f.unwrap()}\")\n\
println(\"bools=${count_via_for(BoolSrc { n: 0 })}\")\n\
println(\"floats=${count_via_for(FloatSrc { n: 0 })}\")\n\
}";
let dir = std::env::temp_dir().join("nova-iterator-for-generic-bound");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("bool=true\nfloat=2.5\nbools=3\nfloats=2\n");
}
#[test]
fn iterator_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/iterator.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/iterator.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn iterator_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/iterator.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/iterator.nova", "iterator");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn iterator_under_gc_stress() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/iterator.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/iterator.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn a_wrong_instantiation_of_a_projection_shaped_record_is_e0079_at_construction() {
let src = "fn main() {\n\
let m = MapIter { it: 5, f: |x| x }\n\
println(\"built\")\n\
}";
let dir = std::env::temp_dir().join("nova-record-bound-e0079");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("error[E0079]") && stderr.contains("`Int::Item`"),
"stderr: {stderr}"
);
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).to_string();
assert!(
!stdout.contains("built"),
"rejected before `main` ran: {stdout}"
);
}
#[test]
fn an_unused_record_bound_is_still_enforced_through_a_bounded_impl_method() {
let src = "record Boxed<K: Hash + Eq, V> { k: K\n\
v: V }\n\
impl<K: Hash + Eq, V> Boxed<K, V> {\n\
fn key(self) -> K { self.k }\n\
}\n\
record NoHash { z: Int }\n\
fn main() {\n\
let b = Boxed { k: NoHash { z: 1 }, v: 7 }\n\
let k = b.key()\n\
println(\"${k.z}\")\n\
}";
let dir = std::env::temp_dir().join("nova-record-bound-e0013");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("error[E0013]") && stderr.contains("`NoHash: Hash`"),
"stderr: {stderr}"
);
assert!(
stderr.contains("`NoHash: Eq`"),
"one diagnostic per unsatisfied bound, not just the first: {stderr}"
);
}
#[test]
fn a_record_bound_no_field_type_uses_is_silently_accepted_when_never_exercised() {
let src = "record Boxed<K: Hash + Eq, V> { k: K\n\
v: V }\n\
impl<K: Hash + Eq, V> Boxed<K, V> {\n\
fn key(self) -> K { self.k }\n\
}\n\
record NoHash { z: Int }\n\
fn main() {\n\
let b = Boxed { k: NoHash { z: 1 }, v: 7 }\n\
println(\"${b.v}\")\n\
}";
let dir = std::env::temp_dir().join("nova-record-bound-accepted");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, src).expect("write");
nova()
.arg("run")
.arg(&path)
.assert()
.success()
.stdout("7\n");
}
#[test]
fn a_test_binary_with_no_index_prints_its_inventory() {
let dir = std::env::temp_dir().join("nova-test-inventory");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("inventory.nova");
std::fs::write(&file, "@test\nfn alpha() { }\n@test\nfn beta() { }\n").expect("write");
let (exe, tests) =
nova_driver::build_test_binary(&file).expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["alpha", "beta"],
"collection order feeds the printed inventory directly"
);
let assert = Command::new(&exe)
.env_remove("NOVA_TEST_INDEX")
.assert()
.success();
let out = String::from_utf8(assert.get_output().stdout.clone())
.expect("stdout is UTF-8")
.replace("\r\n", "\n");
assert_eq!(out, "2\nalpha\nbeta\n");
let _ = std::fs::remove_file(&exe);
}
#[test]
fn a_test_binary_runs_exactly_the_selected_test() {
let dir = std::env::temp_dir().join("nova-test-selected");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("selected.nova");
std::fs::write(
&file,
"@test\nfn alpha() { println(\"A\") }\n\
@test\nfn beta() { println(\"B\") }\n",
)
.expect("write");
let (exe, tests) =
nova_driver::build_test_binary(&file).expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["alpha", "beta"],
"index 1 below selects beta only if collection order is [alpha, beta]"
);
let assert = Command::new(&exe)
.env("NOVA_TEST_INDEX", "1")
.assert()
.success();
let out = String::from_utf8(assert.get_output().stdout.clone())
.expect("stdout is UTF-8")
.replace("\r\n", "\n");
assert_eq!(out, "B\n");
let _ = std::fs::remove_file(&exe);
}
#[test]
fn a_test_binary_runs_the_selected_test_not_the_users_own_main() {
let dir = std::env::temp_dir().join("nova-test-user-main");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("user_main.nova");
std::fs::write(
&file,
"@test\nfn alpha() { println(\"TEST-RAN\") }\n\
fn main() { println(\"USER-MAIN-RAN\") }\n",
)
.expect("write");
let (exe, tests) =
nova_driver::build_test_binary(&file).expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["alpha"]
);
let assert = Command::new(&exe)
.env("NOVA_TEST_INDEX", "0")
.assert()
.success();
let out = String::from_utf8(assert.get_output().stdout.clone())
.expect("stdout is UTF-8")
.replace("\r\n", "\n");
assert_eq!(out, "TEST-RAN\n");
let _ = std::fs::remove_file(&exe);
}
#[test]
fn test_binary_output_paths_do_not_collide_across_same_stem_sources() {
let dir_a = std::env::temp_dir().join("nova-test-stem-collision-a");
let dir_b = std::env::temp_dir().join("nova-test-stem-collision-b");
std::fs::create_dir_all(&dir_a).expect("temp dir a");
std::fs::create_dir_all(&dir_b).expect("temp dir b");
let file_a = dir_a.join("main.nova");
let file_b = dir_b.join("main.nova");
std::fs::write(&file_a, "@test\nfn from_a() { }\n").expect("write a");
std::fs::write(&file_b, "@test\nfn from_b() { }\n").expect("write b");
let (exe_a, _) = nova_driver::build_test_binary(&file_a).expect("a compiles and links");
let (exe_b, _) = nova_driver::build_test_binary(&file_b).expect("b compiles and links");
assert_ne!(
exe_a, exe_b,
"two different main.nova files must not share an output path"
);
assert!(exe_a.exists(), "a's binary exists at {}", exe_a.display());
assert!(exe_b.exists(), "b's binary exists at {}", exe_b.display());
let _ = std::fs::remove_file(&exe_a);
let _ = std::fs::remove_file(&exe_b);
}
#[test]
fn assert_eq_failure_names_both_values() {
let dir = std::env::temp_dir().join("nova-test-assert-eq-both-values");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("both_values.nova");
std::fs::write(&file, "@test\nfn t() { assert_eq(1, 3) }\n").expect("write");
let (exe, tests) =
nova_driver::build_test_binary(&file).expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["t"]
);
let assert = Command::new(&exe)
.env("NOVA_TEST_INDEX", "0")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).replace("\r\n", "\n");
assert_eq!(
stderr.trim_end(),
"nova: panic: assertion failed: 1 != 3",
"stderr: {stderr}"
);
let _ = std::fs::remove_file(&exe);
}
#[test]
fn assert_eq_works_at_bool_and_float_not_only_int() {
let dir = std::env::temp_dir().join("nova-test-assert-eq-types");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("types.nova");
std::fs::write(
&file,
"@test\nfn int_pass() { assert_eq(1, 1) }\n\
@test\nfn int_fail() { assert_eq(1, 2) }\n\
@test\nfn bool_pass() { assert_eq(true, true) }\n\
@test\nfn bool_fail() { assert_eq(true, false) }\n\
@test\nfn float_pass() { assert_eq(0.5, 0.5) }\n\
@test\nfn float_fail() { assert_eq(0.5, 1.5) }\n\
@test\nfn string_pass() { assert_eq(\"a\", \"a\") }\n\
@test\nfn string_fail() { assert_eq(\"a\", \"b\") }\n",
)
.expect("write");
let (exe, tests) =
nova_driver::build_test_binary(&file).expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec![
"int_pass",
"int_fail",
"bool_pass",
"bool_fail",
"float_pass",
"float_fail",
"string_pass",
"string_fail",
],
"collection order fixes which index below selects which case"
);
let cases: [(usize, Option<&str>); 8] = [
(0, None),
(1, Some("nova: panic: assertion failed: 1 != 2")),
(2, None),
(3, Some("nova: panic: assertion failed: true != false")),
(4, None),
(5, Some("nova: panic: assertion failed: 0.5 != 1.5")),
(6, None),
(7, Some("nova: panic: assertion failed: \"a\" != \"b\"")),
];
for (index, expected_panic) in cases {
let assert = Command::new(&exe)
.env("NOVA_TEST_INDEX", index.to_string())
.assert();
match expected_panic {
None => {
let assert = assert.success();
let out = String::from_utf8_lossy(&assert.get_output().stdout).to_string();
assert_eq!(
out, "",
"a passing assert_eq must print nothing (index {index})"
);
}
Some(msg) => {
let assert = assert.failure();
let stderr =
String::from_utf8_lossy(&assert.get_output().stderr).replace("\r\n", "\n");
assert_eq!(stderr.trim_end(), msg, "index {index}");
}
}
}
let _ = std::fs::remove_file(&exe);
}
#[test]
fn assert_is_not_available_outside_nova_test() {
let dir = std::env::temp_dir().join("nova-test-assert-not-available");
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("main.nova");
std::fs::write(&path, "fn main() { assert(true, \"x\") }\n").expect("write");
let assert = nova().arg("run").arg(&path).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0001"), "stderr: {stderr}");
assert!(
stderr.contains("cannot find function `assert` in this scope"),
"stderr: {stderr}"
);
}
#[test]
fn assert_panics_with_its_message_and_is_silent_when_true() {
let dir = std::env::temp_dir().join("nova-test-assert-basic");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("basic.nova");
std::fs::write(
&file,
"@test\nfn assert_pass() { assert(true, \"boom\") }\n\
@test\nfn assert_fail() { assert(false, \"boom\") }\n",
)
.expect("write");
let (exe, tests) =
nova_driver::build_test_binary(&file).expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["assert_pass", "assert_fail"]
);
let pass = Command::new(&exe)
.env("NOVA_TEST_INDEX", "0")
.assert()
.success();
let out = String::from_utf8_lossy(&pass.get_output().stdout).to_string();
assert_eq!(out, "", "assert(true, ..) must print nothing");
let fail = Command::new(&exe)
.env("NOVA_TEST_INDEX", "1")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&fail.get_output().stderr).replace("\r\n", "\n");
assert_eq!(
stderr.trim_end(),
"nova: panic: assertion failed: boom",
"stderr: {stderr}"
);
let _ = std::fs::remove_file(&exe);
}
#[test]
fn assert_ne_panics_when_equal_and_is_silent_when_different() {
let dir = std::env::temp_dir().join("nova-test-assert-ne-basic");
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("basic.nova");
std::fs::write(
&file,
"@test\nfn ne_pass() { assert_ne(1, 2) }\n\
@test\nfn ne_fail() { assert_ne(1, 1) }\n",
)
.expect("write");
let (exe, tests) =
nova_driver::build_test_binary(&file).expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["ne_pass", "ne_fail"]
);
let pass = Command::new(&exe)
.env("NOVA_TEST_INDEX", "0")
.assert()
.success();
let out = String::from_utf8_lossy(&pass.get_output().stdout).to_string();
assert_eq!(out, "", "assert_ne(1, 2) must print nothing");
let fail = Command::new(&exe)
.env("NOVA_TEST_INDEX", "1")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&fail.get_output().stderr).replace("\r\n", "\n");
assert_eq!(
stderr.trim_end(),
"nova: panic: assertion failed: 1 == 1",
"stderr: {stderr}"
);
let _ = std::fs::remove_file(&exe);
}
fn write_test_project(unique_name: &str, source: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(unique_name);
std::fs::create_dir_all(dir.join("src")).expect("temp project dir");
std::fs::write(dir.join("src/main.nova"), source).expect("write fixture");
dir
}
#[test]
fn nova_test_reports_a_pass_and_a_failure_distinctly() {
let dir = write_test_project(
"nova-test-cli-pass-fail",
"@test\nfn addition_works() { assert_eq(1 + 1, 2) }\n\
@test\nfn addition_is_broken() { assert_eq(1 + 1, 3) }\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 2 tests\n\
test addition_works ... ok\n\
test addition_is_broken ... FAILED\n\
\x20\x20\x20\x20nova: panic: assertion failed: 2 != 3\n\
\n\
test result: FAILED. 1 passed; 1 failed; 0 trapped; 2 total\n"
);
}
fn expect_trap_exit_code(status: &std::process::ExitStatus) -> (i32, u32) {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert!(
status.signal().is_some(),
"a hard trap must be killed by a fatal signal on Unix, not merely exit nonzero: {status:?}"
);
let code = -1i32;
(code, code as u32)
}
#[cfg(windows)]
{
let code = status
.code()
.expect("a concrete exit code on this platform");
(code, code as u32)
}
}
#[test]
fn a_hard_trap_is_reported_as_a_trap_and_does_not_satisfy_should_panic() {
let dir = write_test_project(
"nova-test-cli-hard-trap",
"@test(should_panic)\nfn divides_by_zero() { let _ = 1 / 0 }\n",
);
let (exe, tests) = nova_driver::build_test_binary(&dir.join("src/main.nova"))
.expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["divides_by_zero"]
);
let direct = Command::new(&exe)
.env("NOVA_TEST_INDEX", "0")
.output()
.expect("run the trap directly");
assert!(
!direct.status.success(),
"a division by zero must exit nonzero"
);
let direct_stderr = String::from_utf8_lossy(&direct.stderr);
assert!(
!direct_stderr.contains("nova: panic:"),
"must be a hard trap with no panic message, not a checked panic: {direct_stderr}"
);
let (code, hex) = expect_trap_exit_code(&direct.status);
let _ = std::fs::remove_file(&exe);
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
format!(
"running 1 test\n\
test divides_by_zero ... TRAPPED (exit code {code} (0x{hex:08X}))\n\
\n\
test result: FAILED. 0 passed; 0 failed; 1 trapped; 1 total\n"
)
);
}
#[test]
fn should_panic_passes_on_a_checked_panic() {
let dir = write_test_project(
"nova-test-cli-should-panic-checked",
"@test(should_panic)\nfn out_of_bounds_panics() { let xs = [1, 2, 3]\n let _ = xs[7] }\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 1 test\n\
test out_of_bounds_panics ... ok\n\
\n\
test result: ok. 1 passed; 0 failed; 0 trapped; 1 total\n"
);
}
#[test]
fn should_panic_fails_distinctly_when_the_test_does_not_panic() {
let dir = write_test_project(
"nova-test-cli-should-panic-not-panicking",
"@test(should_panic)\nfn does_not_actually_panic() { assert_eq(1, 1) }\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 1 test\n\
test does_not_actually_panic ... FAILED (expected a panic, but the test passed)\n\
\n\
test result: FAILED. 0 passed; 1 failed; 0 trapped; 1 total\n"
);
}
#[test]
fn join_handle_rejects_a_never_spawned_future_and_a_live_duplicate() {
let dir = write_test_project(
"nova-test-join-handle-forgery-and-duplicate",
"async fn spin() -> Int { 1 }\n\
\n\
@test(should_panic)\n\
fn joining_a_never_spawned_handle_aborts() {\n\
\x20\x20let h = JoinHandle { fut: spin() }\n\
\x20\x20let _ = block_on(h.join())\n\
}\n\
\n\
@test(should_panic)\n\
fn spawning_the_same_future_twice_aborts() {\n\
\x20\x20let f = spin()\n\
\x20\x20spawn(f)\n\
\x20\x20let _ = spawn(f)\n\
}\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 2 tests\n\
test joining_a_never_spawned_handle_aborts ... ok\n\
test spawning_the_same_future_twice_aborts ... ok\n\
\n\
test result: ok. 2 passed; 0 failed; 0 trapped; 2 total\n"
);
}
#[test]
fn spawning_two_distinct_futures_from_the_same_call_site_still_works() {
let dir = write_test_project(
"nova-test-join-handle-double-spawn-guard",
"async fn spin() -> Int { 1 }\n\
async fn run_two_spawns() -> Int {\n\
\x20\x20let a = spawn(spin())\n\
\x20\x20let b = spawn(spin())\n\
\x20\x20let ra = a.join().await\n\
\x20\x20let rb = b.join().await\n\
\x20\x20ra + rb\n\
}\n\
\n\
@test\n\
fn spawning_two_distinct_futures_still_works() {\n\
\x20\x20assert_eq(block_on(run_two_spawns()), 2)\n\
}\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 1 test\n\
test spawning_two_distinct_futures_still_works ... ok\n\
\n\
test result: ok. 1 passed; 0 failed; 0 trapped; 1 total\n"
);
}
#[test]
fn joining_a_handle_twice_returns_the_same_value_twice_with_no_abort() {
let dir = write_test_project(
"nova-test-join-handle-join-twice",
"async fn spin() -> Int { 1 }\n\
async fn run_join_twice() -> Int {\n\
\x20\x20let h = spawn(spin())\n\
\x20\x20let a = h.join().await\n\
\x20\x20let b = h.join().await\n\
\x20\x20a + b\n\
}\n\
\n\
@test\n\
fn joining_a_handle_twice_returns_the_same_value_twice() {\n\
\x20\x20assert_eq(block_on(run_join_twice()), 2)\n\
}\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 1 test\n\
test joining_a_handle_twice_returns_the_same_value_twice ... ok\n\
\n\
test result: ok. 1 passed; 0 failed; 0 trapped; 1 total\n"
);
}
#[test]
fn a_filter_selects_a_strict_subset_and_the_others_do_not_run() {
let dir = write_test_project(
"nova-test-cli-filter",
"@test\nfn keep_a() { assert_eq(1, 1) }\n\
@test\nfn keep_b() { assert_eq(2, 2) }\n\
@test\nfn skip_c() {\n\
\x20\x20\x20\x20let mut i = 0\n\
\x20\x20\x20\x20while i < 40000000000 { i = i + 1 }\n\
\x20\x20\x20\x20assert_eq(i, 40000000000)\n\
}\n",
);
let start = std::time::Instant::now();
let assert = nova()
.current_dir(&dir)
.arg("test")
.arg("keep")
.assert()
.success();
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(15),
"the excluded test's ~25s loop must not have run; took {elapsed:?}"
);
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 2 tests\n\
test keep_a ... ok\n\
test keep_b ... ok\n\
\n\
test result: ok. 2 passed; 0 failed; 0 trapped; 2 total\n"
);
}
#[test]
fn should_panic_is_matched_to_its_own_test_not_to_index_zero() {
let dir = write_test_project(
"nova-test-cli-should-panic-by-index",
"@test\nfn alpha_ok() { assert_eq(1, 1) }\n\
@test(should_panic)\nfn beta_panics_checked() { let xs = [1, 2, 3]\n let _ = xs[9] }\n\
@test\nfn gamma_ok() { assert_eq(2, 2) }\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 3 tests\n\
test alpha_ok ... ok\n\
test beta_panics_checked ... ok\n\
test gamma_ok ... ok\n\
\n\
test result: ok. 3 passed; 0 failed; 0 trapped; 3 total\n"
);
}
#[test]
fn a_filter_matches_a_substring_anywhere_not_only_as_a_prefix() {
let dir = write_test_project(
"nova-test-cli-filter-substring-not-prefix",
"@test\nfn zzz_alpha_zzz() { assert_eq(1, 1) }\n\
@test\nfn keep_alpha() { assert_eq(2, 2) }\n\
@test\nfn unrelated_beta() { assert_eq(3, 3) }\n",
);
let assert = nova()
.current_dir(&dir)
.arg("test")
.arg("alpha")
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 2 tests\n\
test zzz_alpha_zzz ... ok\n\
test keep_alpha ... ok\n\
\n\
test result: ok. 2 passed; 0 failed; 0 trapped; 2 total\n"
);
}
fn write_gate_fixture_project(unique_name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(unique_name);
std::fs::create_dir_all(dir.join("src")).expect("temp project dir");
let source = std::fs::read_to_string(repo_root().join("tests/runtime/nova_test.nova"))
.expect("gate fixture exists");
std::fs::write(dir.join("src/main.nova"), source).expect("write fixture");
dir
}
fn read_gate_expected() -> String {
std::fs::read_to_string(repo_root().join("tests/runtime/nova_test.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n")
}
fn normalize_trap_codes(s: &str) -> String {
const MARKER: &str = "TRAPPED (exit code ";
s.split('\n')
.map(|line| match line.find(MARKER) {
Some(i) if line.ends_with("))") => {
format!("{}{}<CODE> (<HEX>))", &line[..i], MARKER)
}
_ => line.to_string(),
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn normalize_trap_codes_elides_only_the_platform_specific_numbers() {
let win = "test t ... TRAPPED (exit code -1073741795 (0xC000001D))\n";
let unix = "test t ... TRAPPED (exit code -1 (0xFFFFFFFF))\n";
assert_eq!(normalize_trap_codes(win), normalize_trap_codes(unix));
assert_eq!(
normalize_trap_codes(win),
"test t ... TRAPPED (exit code <CODE> (<HEX>))\n"
);
assert_ne!(
normalize_trap_codes(win),
normalize_trap_codes("test t ... TRAPPED (exit code -1073741795)\n")
);
assert_ne!(
normalize_trap_codes(win),
normalize_trap_codes("test OTHER ... TRAPPED (exit code -1073741795 (0xC000001D))\n")
);
assert_ne!(
normalize_trap_codes(win),
normalize_trap_codes("test t ... FAILED (exit code -1073741795 (0xC000001D))\n")
);
let untouched = "running 4 tests\ntest a ... ok\n\ntest result: FAILED. 2 passed; 1 failed; 1 trapped; 4 total\n";
assert_eq!(normalize_trap_codes(untouched), untouched);
let decoy = " nova: panic: exit code -5 is wrong\n";
assert_eq!(normalize_trap_codes(decoy), decoy);
}
#[test]
fn nova_test_run() {
let expected = read_gate_expected();
let dir = write_gate_fixture_project("nova-test-gate-run");
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
assert_eq!(
normalize_trap_codes(&stdout),
expected,
"gate output mismatch. Raw, un-normalized stdout follows — the exit \
codes in it are the evidence ADR 0008's open question needs, and \
normalization would otherwise replace them with placeholders. \
stderr follows too, in case stdout above is empty:\nstdout:\n{stdout}\n\
stderr:\n{stderr}"
);
}
#[test]
fn nova_test_build_standalone() {
let expected = read_gate_expected();
let dir = write_gate_fixture_project("nova-test-gate-build-standalone");
let (exe, tests) = nova_driver::build_test_binary(&dir.join("src/main.nova"))
.expect("test binary compiles and links");
let mut out = format!(
"running {} test{}\n",
tests.len(),
if tests.len() == 1 { "" } else { "s" }
);
let (mut passed, mut failed, mut trapped) = (0usize, 0usize, 0usize);
for (i, t) in tests.iter().enumerate() {
let result = Command::new(&exe)
.env("NOVA_TEST_INDEX", i.to_string())
.output()
.unwrap_or_else(|e| panic!("failed to run {} for `{}`: {e}", exe.display(), t.name));
let stderr = String::from_utf8_lossy(&result.stderr);
let panic_line = stderr.lines().find(|line| line.contains("nova: panic:"));
if result.status.success() {
if t.should_panic {
failed += 1;
out.push_str(&format!(
"test {} ... FAILED (expected a panic, but the test passed)\n",
t.name
));
} else {
passed += 1;
out.push_str(&format!("test {} ... ok\n", t.name));
}
} else if let Some(line) = panic_line {
if t.should_panic {
passed += 1;
out.push_str(&format!("test {} ... ok\n", t.name));
} else {
failed += 1;
out.push_str(&format!(
"test {} ... FAILED\n {}\n",
t.name,
line.trim()
));
}
} else {
trapped += 1;
let code = result.status.code().unwrap_or(-1);
let hex = code as u32;
out.push_str(&format!(
"test {} ... TRAPPED (exit code {code} (0x{hex:08X}))\n",
t.name
));
}
}
out.push('\n');
let total = tests.len();
let all_ok = failed == 0 && trapped == 0;
out.push_str(&format!(
"test result: {}. {passed} passed; {failed} failed; {trapped} trapped; {total} total\n",
if all_ok { "ok" } else { "FAILED" }
));
let _ = std::fs::remove_file(&exe);
assert_eq!(
normalize_trap_codes(&out.replace("\r\n", "\n")),
expected,
"gate output mismatch. Raw, un-normalized report follows — the exit \
codes in it are the evidence ADR 0008's open question needs, and \
normalization would otherwise replace them with placeholders:\n{out}"
);
}
#[test]
fn nova_test_under_gc_stress() {
let expected = read_gate_expected();
let dir = write_gate_fixture_project("nova-test-gate-gc-stress");
let assert = nova()
.env("NOVA_GC_STRESS", "1")
.current_dir(&dir)
.arg("test")
.assert()
.failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
assert_eq!(
normalize_trap_codes(&stdout),
expected,
"gate output mismatch. Raw, un-normalized stdout follows — the exit \
codes in it are the evidence ADR 0008's open question needs, and \
normalization would otherwise replace them with placeholders. \
stderr follows too, in case stdout above is empty:\nstdout:\n{stdout}\n\
stderr:\n{stderr}"
);
}
#[test]
fn nova_test_filter_run() {
let dir = write_gate_fixture_project("nova-test-gate-filter");
let assert = nova()
.current_dir(&dir)
.arg("test")
.arg("is_correct")
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 1 test\n\
test addition_is_correct ... ok\n\
\n\
test result: ok. 1 passed; 0 failed; 0 trapped; 1 total\n"
);
}
#[test]
fn nova_test_reports_e0082_for_an_unknown_attribute() {
let dir = write_test_project("nova-test-gate-e0082", "@tset\nfn foo() { }\n");
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
assert!(stderr.contains("E0082"), "stderr: {stderr}");
assert!(
stderr.contains("unknown attribute `@tset`; known attributes are: test"),
"stderr: {stderr}"
);
}
#[test]
fn nova_test_rejects_an_async_test_function() {
let dir = write_test_project(
"nova-test-async-rejected",
"async fn helper() -> Int { 41 }\n\
@test\nasync fn t() { assert_eq(helper().await, 999) }\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
assert!(stderr.contains("E0084"), "stderr: {stderr}");
assert!(
stderr.contains("a `@test` function may not have an `async` body"),
"the message must name `async`, not a generic bad signature: {stderr}"
);
assert!(
stderr.contains("block_on"),
"the diagnostic must name the working alternative: {stderr}"
);
assert!(
stderr.contains("@test fn t()"),
"the diagnostic must spell the replacement out as source: {stderr}"
);
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert!(
!stdout.contains("... ok"),
"no test may be reported as passing: {stdout}"
);
}
#[test]
fn nova_test_runs_an_async_body_via_block_on_and_pins_a_wrong_answer() {
let dir = write_test_project(
"nova-test-async-block-on",
"async fn helper() -> Int {\n yield_now().await\n 41\n}\n\
@test\nfn an_async_body_runs_under_block_on() { assert_eq(block_on(helper()), 41) }\n\
@test\nfn a_wrong_answer_inside_block_on_fails() { assert_eq(block_on(helper()), 999) }\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 2 tests\n\
test an_async_body_runs_under_block_on ... ok\n\
test a_wrong_answer_inside_block_on_fails ... FAILED\n\
\x20\x20\x20\x20nova: panic: assertion failed: 41 != 999\n\
\n\
test result: FAILED. 1 passed; 1 failed; 0 trapped; 2 total\n"
);
}
#[test]
fn test_functions_calling_assert_eq_do_not_break_check_build_or_run() {
let dir = write_test_project(
"nova-test-cli-critical-nontest-compile",
"@test\nfn addition_is_correct() { assert_eq(2 + 2, 4) }\n\
@test\nfn addition_is_wrong() { assert_eq(2 + 2, 5) }\n\
fn main() { println(\"hello\") }\n",
);
let file = dir.join("src/main.nova");
nova().arg("check").arg(&file).assert().success();
let out_exe = dir.join(format!("built{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&out_exe)
.assert()
.success();
let run = Command::new(&out_exe).output().expect("run built binary");
assert!(run.status.success());
assert_eq!(
String::from_utf8_lossy(&run.stdout).replace("\r\n", "\n"),
"hello\n"
);
let _ = std::fs::remove_file(&out_exe);
nova()
.arg("run")
.arg(&file)
.assert()
.success()
.stdout("hello\n");
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 2 tests\n\
test addition_is_correct ... ok\n\
test addition_is_wrong ... FAILED\n\
\x20\x20\x20\x20nova: panic: assertion failed: 4 != 5\n\
\n\
test result: FAILED. 1 passed; 1 failed; 0 trapped; 2 total\n"
);
}
#[test]
fn test_functions_in_an_imported_module_do_not_break_check_or_build() {
let dir = write_test_project(
"nova-test-cli-critical-nontest-compile-imported",
"import helper\nfn main() { println(\"hello\") }\n",
);
std::fs::write(
dir.join("src/helper.nova"),
"@test\nfn helper_test() { assert_eq(1 + 1, 2) }\n",
)
.expect("write imported module");
let file = dir.join("src/main.nova");
nova().arg("check").arg(&file).assert().success();
let out_exe = dir.join(format!("built-imported{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&out_exe)
.assert()
.success();
let _ = std::fs::remove_file(&out_exe);
}
#[test]
fn a_failing_tests_own_stdout_is_no_longer_discarded() {
let dir = write_test_project(
"nova-test-cli-captured-stdout-on-failure",
"@test\nfn prints_then_fails() {\n\
\x20\x20\x20\x20println(\"about to fail\")\n\
\x20\x20\x20\x20assert_eq(1, 2)\n\
}\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 1 test\n\
test prints_then_fails ... FAILED\n\
\x20\x20\x20\x20nova: panic: assertion failed: 1 != 2\n\
\x20\x20\x20\x20---- stdout ----\n\
\x20\x20\x20\x20about to fail\n\
\n\
test result: FAILED. 0 passed; 1 failed; 0 trapped; 1 total\n"
);
}
#[test]
fn a_trapping_tests_captured_output_is_shown_not_discarded() {
let dir = write_test_project(
"nova-test-cli-captured-output-on-trap",
"@test(should_panic)\nfn prints_then_traps() {\n\
\x20\x20\x20\x20println(\"about to trap\")\n\
\x20\x20\x20\x20let _ = 1 / 0\n\
}\n",
);
let (exe, tests) = nova_driver::build_test_binary(&dir.join("src/main.nova"))
.expect("test binary compiles and links");
assert_eq!(
tests.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
vec!["prints_then_traps"]
);
let direct = Command::new(&exe)
.env("NOVA_TEST_INDEX", "0")
.output()
.expect("run the trap directly");
let (code, hex) = expect_trap_exit_code(&direct.status);
let _ = std::fs::remove_file(&exe);
let assert = nova().current_dir(&dir).arg("test").assert().failure();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
format!(
"running 1 test\n\
test prints_then_traps ... TRAPPED (exit code {code} (0x{hex:08X}))\n\
\x20\x20\x20\x20---- stdout ----\n\
\x20\x20\x20\x20about to trap\n\
\n\
test result: FAILED. 0 passed; 0 failed; 1 trapped; 1 total\n"
)
);
}
#[test]
fn an_explicit_filter_matching_nothing_is_a_failure() {
let dir = write_test_project(
"nova-test-cli-filter-matches-nothing",
"@test\nfn addition_is_correct() { assert_eq(1 + 1, 2) }\n",
);
let assert = nova()
.current_dir(&dir)
.arg("test")
.arg("nosuchtest")
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
assert!(
stderr.contains("no test name contains the filter `nosuchtest`"),
"stderr: {stderr}"
);
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert_eq!(stdout, "", "stdout: {stdout}");
}
#[test]
fn an_unfiltered_run_with_no_tests_still_succeeds() {
let dir = write_test_project("nova-test-cli-no-tests-at-all", "fn main() { }\n");
let assert = nova().current_dir(&dir).arg("test").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 0 tests\n\
\n\
test result: ok. 0 passed; 0 failed; 0 trapped; 0 total\n"
);
}
#[test]
fn a_test_program_may_hold_function_values() {
let dir = write_test_project(
"nova-test-cli-function-values",
"fn inc(x: Int) -> Int { x + 1 }\n\
fn apply(f: fn(Int) -> Int, x: Int) -> Int { f(x) }\n\
@test\nfn a_closure() {\n let f = |x: Int| x + 1\n assert_eq(f(1), 2)\n}\n\
@test\nfn a_named_function_as_a_value() {\n assert_eq(apply(inc, 1), 2)\n \
assert_eq(Some(1).map(inc).unwrap_or(0), 2)\n}\n",
);
let assert = nova().current_dir(&dir).arg("test").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).replace("\r\n", "\n");
assert_eq!(
stdout,
"running 2 tests\n\
test a_closure ... ok\n\
test a_named_function_as_a_value ... ok\n\
\n\
test result: ok. 2 passed; 0 failed; 0 trapped; 2 total\n"
);
}
#[test]
fn fs_io_types_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fs_io_types.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_io_types.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn eprint_family_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/eprint_family.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/eprint_family.nova"))
.assert()
.success()
.stdout(expected)
.stderr("ab\n");
}
#[test]
fn fs_not_found_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fs_not_found.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_not_found.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn fs_not_found_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fs_not_found.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/fs_not_found.nova", "fs_not_found");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[cfg(windows)]
#[test]
fn fs_permission_denied_run() {
let tmp = unique_temp_dir("nova-fs-permission-denied");
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fs_permission_denied.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_permission_denied.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn fs_invalid_data_run() {
let tmp = unique_temp_dir("nova-fs-invalid-data");
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fs_invalid_data.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_invalid_data.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn fs_write_then_read_run() {
let tmp = unique_temp_dir("nova-fs-write-then-read");
let path = tmp.join("nova_fs_write_then_read_8a41.txt");
let _ = std::fs::remove_file(&path);
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fs_write_then_read.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_write_then_read.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
assert!(
path.exists(),
"the write should have landed at {} (the path this harness computed \
from its own TMPDIR/TMP/TEMP override), not merely somewhere writable",
path.display()
);
let _ = std::fs::remove_dir_all(&tmp);
}
fn unique_temp_dir(label: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("{label}-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("unique temp dir for a std/fs fixture");
dir
}
#[test]
fn fs_roundtrip_run() {
let tmp = unique_temp_dir("nova-fs-roundtrip");
let written_path = tmp.join("nova_fs_roundtrip_5d2c.txt");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fs_roundtrip.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_roundtrip.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let removed = !written_path.exists();
let _ = std::fs::remove_dir_all(&tmp);
assert!(
removed,
"remove_file should have actually deleted {}, not merely reported success",
written_path.display()
);
}
#[test]
fn fs_already_exists_run() {
let tmp = unique_temp_dir("nova-fs-already-exists");
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fs_already_exists.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_already_exists.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn fs_dirs_run() {
let tmp = unique_temp_dir("nova-fs-dirs");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fs_dirs.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_dirs.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn fs_read_dir_run() {
let tmp = unique_temp_dir("nova-fs-read-dir");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fs_read_dir.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_read_dir.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn fs_read_dir_under_gc_stress() {
let tmp = unique_temp_dir("nova-fs-read-dir-stress");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fs_read_dir.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/fs_read_dir.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn bytes_reserved_declaration_is_rejected() {
let file = repo_root().join("tests/runtime/bytes_reserved.nova");
let assert = nova().arg("check").arg(&file).assert().failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("E0089"), "stderr: {stderr}");
}
#[test]
fn bytes_basics_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/bytes_basics.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/bytes_basics.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn bytes_api_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/bytes_api.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/bytes_api.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn bytes_api_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/bytes_api.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = build_and_run("tests/runtime/bytes_api.nova", "bytes_api");
assert_eq!(out.replace("\r\n", "\n"), expected);
}
#[test]
fn bytes_api_under_gc_stress() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/bytes_api.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/bytes_api.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn bytes_from_ints_rejects_a_value_above_the_range() {
let assert = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/bytes_from_ints_too_high.nova"))
.assert()
.failure();
let out = assert.get_output();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("nova: panic: nova_rt_bytes_from_ints: element out of range 0..=255"),
"stderr was {stderr:?}"
);
assert!(!stdout.contains('1'), "stdout was {stdout:?}");
}
#[test]
fn bytes_from_ints_rejects_a_value_below_the_range() {
let assert = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/bytes_from_ints_too_low.nova"))
.assert()
.failure();
let out = assert.get_output();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("nova: panic: nova_rt_bytes_from_ints: element out of range 0..=255"),
"stderr was {stderr:?}"
);
assert!(!stdout.contains('1'), "stdout was {stdout:?}");
}
#[test]
fn fs_bytes_roundtrip_run() {
let tmp = unique_temp_dir("nova-fs-bytes-roundtrip");
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fs_bytes_roundtrip.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_bytes_roundtrip.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn fs_bytes_roundtrip_under_gc_stress() {
let tmp = unique_temp_dir("nova-fs-bytes-roundtrip-stress");
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fs_bytes_roundtrip.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/fs_bytes_roundtrip.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn fs_bytes_errors_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fs_bytes_errors.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fs_bytes_errors.nova"))
.assert()
.success()
.stdout(expected);
}
fn io_streams_stdout_marker() -> String {
format!("{}\n", "Q".repeat(300))
}
const IO_STREAMS_STDERR_MARKER: &str = "NOVA_STDERR_ONLY_MARKER\n";
#[test]
fn io_streams_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/io_streams.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let assert = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/io_streams.nova"))
.write_stdin("hello-stdin")
.assert()
.success();
let out = assert.get_output();
let stdout = String::from_utf8_lossy(&out.stdout).replace("\r\n", "\n");
let stderr = String::from_utf8_lossy(&out.stderr).replace("\r\n", "\n");
assert_eq!(stdout, expected, "stdout did not match the golden fixture");
assert_eq!(
stderr, IO_STREAMS_STDERR_MARKER,
"stderr must hold exactly the stderr marker and nothing else"
);
let stdout_marker = io_streams_stdout_marker();
assert!(
stdout.contains(&stdout_marker),
"stdout must contain its own marker: {stdout:?}"
);
assert!(
stderr.contains("NOVA_STDERR_ONLY_MARKER"),
"stderr must contain its own marker: {stderr:?}"
);
assert!(
!stdout.contains("NOVA_STDERR_ONLY_MARKER"),
"stdout must not contain stderr's marker: {stdout:?}"
);
assert!(
!stderr.contains(&stdout_marker),
"stderr must not contain stdout's marker: {stderr:?}"
);
}
#[test]
fn io_streams_build_standalone() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/io_streams.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let dir = std::env::temp_dir().join("nova-io-streams-build");
std::fs::create_dir_all(&dir).expect("temp dir");
let exe = dir.join(format!("io_streams{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(repo_root().join("tests/runtime/io_streams.nova"))
.arg("-o")
.arg(&exe)
.assert()
.success();
let assert = Command::new(&exe)
.write_stdin("hello-stdin")
.assert()
.success();
let out = assert.get_output();
let stdout = String::from_utf8_lossy(&out.stdout).replace("\r\n", "\n");
let stderr = String::from_utf8_lossy(&out.stderr).replace("\r\n", "\n");
let _ = std::fs::remove_file(&exe);
assert_eq!(stdout, expected, "stdout did not match the golden fixture");
assert_eq!(
stderr, IO_STREAMS_STDERR_MARKER,
"stderr must hold exactly the stderr marker and nothing else"
);
assert!(
!stdout.contains("NOVA_STDERR_ONLY_MARKER"),
"stdout must not contain stderr's marker: {stdout:?}"
);
assert!(
!stderr.contains(&io_streams_stdout_marker()),
"stderr must not contain stdout's marker: {stderr:?}"
);
}
#[test]
fn io_read_to_end_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/io_read_to_end.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/io_read_to_end.nova"))
.assert()
.success()
.stdout(expected);
}
#[cfg(windows)]
fn run_with_a_broken_pipe(
path: &std::path::Path,
break_stdout: bool,
) -> (std::process::ExitStatus, String) {
use std::io::Read;
use std::process::Stdio;
let mut child = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova"))
.arg("run")
.arg(path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn nova run");
let mut healthy: Box<dyn Read> = if break_stdout {
drop(child.stdout.take().expect("stdout was piped"));
Box::new(child.stderr.take().expect("stderr was piped"))
} else {
drop(child.stderr.take().expect("stderr was piped"));
Box::new(child.stdout.take().expect("stdout was piped"))
};
drop(child.stdin.take().expect("stdin was piped"));
let mut out = String::new();
healthy
.read_to_string(&mut out)
.expect("read the healthy stream to completion");
let status = child.wait().expect("wait for the child");
(status, out)
}
#[cfg(windows)]
#[test]
fn io_broken_stdout_run() {
let (status, stderr) = run_with_a_broken_pipe(
&repo_root().join("tests/runtime/io_broken_stdout.nova"),
true,
);
assert!(status.success(), "the child process itself must exit 0");
assert!(
stderr.contains("write_stdout: err"),
"write_stdout must report Err against a broken pipe: {stderr:?}"
);
assert!(
stderr.contains("write_stdout_buffered: ok"),
"a write with no trailing newline must still buffer successfully: {stderr:?}"
);
assert!(
stderr.contains("flush_stdout: err"),
"flush_stdout must report Err when the deferred write it flushes fails: {stderr:?}"
);
}
#[cfg(windows)]
#[test]
fn io_broken_stderr_run() {
let (status, stdout) = run_with_a_broken_pipe(
&repo_root().join("tests/runtime/io_broken_stderr.nova"),
false,
);
assert!(status.success(), "the child process itself must exit 0");
assert!(
stdout.contains("write_stderr: err"),
"write_stderr must report Err against a broken pipe: {stdout:?}"
);
assert!(
stdout.contains("write_stderr_buffered: err"),
"unlike stdout's, a write to stderr with no trailing newline must \
still fail immediately against a broken pipe -- stderr does not \
line-buffer: {stdout:?}"
);
assert!(
stdout.contains("flush_stderr: ok"),
"with nothing buffered (stderr never buffered either of the writes \
above), flushing stderr must report Ok even though the pipe is \
broken -- there is nothing pending for it to fail on: {stdout:?}"
);
}
#[cfg(windows)]
fn run_with_a_write_only_stdin(path: &std::path::Path) -> (std::process::ExitStatus, String) {
use std::process::Stdio;
let stdin_path = std::env::temp_dir().join("nova_io_read_stdin_write_only_probe.txt");
let stdin_file = std::fs::File::create(&stdin_path)
.expect("create a write-only file to hand the child as its stdin");
let output = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova"))
.arg("run")
.arg(path)
.stdin(Stdio::from(stdin_file))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("run nova with a write-only stdin");
let _ = std::fs::remove_file(&stdin_path);
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
(output.status, stdout)
}
#[cfg(windows)]
#[test]
fn io_read_stdin_write_only_run() {
let (status, stdout) = run_with_a_write_only_stdin(
&repo_root().join("tests/runtime/io_read_stdin_write_only.nova"),
);
assert!(status.success(), "the child process itself must exit 0");
assert!(
stdout.contains("read_stdin: err PermissionDenied"),
"a write-only stdin handle must make read_stdin's intrinsic report \
PermissionDenied, not silently succeed on an empty read: {stdout:?}"
);
}
#[test]
fn file_roundtrip_run() {
let tmp = unique_temp_dir("nova-file-roundtrip");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/file_roundtrip.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/file_roundtrip.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn file_lifetime_run() {
let tmp = unique_temp_dir("nova-file-lifetime");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/file_lifetime.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/file_lifetime.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn file_errors_run() {
let tmp = unique_temp_dir("nova-file-errors");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/file_errors.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/file_errors.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(windows)]
#[test]
fn file_open_dir_run() {
let tmp = unique_temp_dir("nova-file-open-dir");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/file_open_dir.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/file_open_dir.nova"))
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp)
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_dir_all(&tmp);
}
struct EchoServer {
port_path: std::path::PathBuf,
shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl EchoServer {
fn start(label: &str, delay_ms: u64) -> EchoServer {
let listener =
std::net::TcpListener::bind("127.0.0.1:0").expect("bind an ephemeral loopback port");
listener
.set_nonblocking(true)
.expect("listener supports non-blocking accept");
let port = listener
.local_addr()
.expect("bound listener has a local address")
.port();
let port_path = std::env::temp_dir().join(format!("nova_{label}_port.txt"));
std::fs::write(&port_path, port.to_string())
.expect("write the port file the paired fixture reads");
let shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let shutdown_for_thread = std::sync::Arc::clone(&shutdown);
let handle = std::thread::spawn(move || {
let mut stream = loop {
match listener.accept() {
Ok((stream, _)) => break stream,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
if shutdown_for_thread.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
Err(_) => return,
}
};
if stream.set_nonblocking(false).is_err() {
return;
}
if delay_ms > 0 {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
let mut buf = [0u8; 4096];
loop {
match std::io::Read::read(&mut stream, &mut buf) {
Ok(0) | Err(_) => return,
Ok(n) => {
if std::io::Write::write_all(&mut stream, &buf[..n]).is_err() {
return;
}
}
}
}
});
EchoServer {
port_path,
shutdown,
handle: Some(handle),
}
}
}
impl Drop for EchoServer {
fn drop(&mut self) {
self.shutdown
.store(true, std::sync::atomic::Ordering::SeqCst);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
let _ = std::fs::remove_file(&self.port_path);
}
}
fn write_refused_port_file(label: &str) -> std::path::PathBuf {
let listener =
std::net::TcpListener::bind("127.0.0.1:0").expect("bind an ephemeral loopback port");
let port = listener
.local_addr()
.expect("bound listener has a local address")
.port();
drop(listener);
let port_path = std::env::temp_dir().join(format!("nova_{label}_port.txt"));
std::fs::write(&port_path, port.to_string())
.expect("write the port file the paired fixture reads");
port_path
}
#[test]
fn net_roundtrip_run() {
let _server = EchoServer::start("net_roundtrip", 0);
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/net_roundtrip.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/net_roundtrip.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn net_interleave_run() {
let _server = EchoServer::start("net_interleave", 150);
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/net_interleave.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/net_interleave.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn net_timeout_run() {
let _server = EchoServer::start("net_timeout", 250);
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/net_timeout.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/net_timeout.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn net_refused_run() {
let port_path = write_refused_port_file("net_refused");
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/net_refused.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/net_refused.nova"))
.assert()
.success()
.stdout(expected);
let _ = std::fs::remove_file(&port_path);
}
#[test]
fn net_lifetime_run() {
let _server = EchoServer::start("net_lifetime", 0);
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/net_lifetime.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/net_lifetime.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn net_listener_accept_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/net_listener_accept.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/net_listener_accept.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn time_conversions_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/time_conversions.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/time_conversions.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn time_saturation_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/time_saturation.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/time_saturation.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn time_elapsed_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/time_elapsed.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/time_elapsed.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn system_time_iso8601_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/system_time_iso8601.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/system_time_iso8601.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn system_time_now_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/system_time_now.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/system_time_now.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn timeout_ok_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/timeout_ok.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/timeout_ok.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn timeout_elapsed_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/timeout_elapsed.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/timeout_elapsed.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn timeout_value_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/timeout_value.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/timeout_value.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn timeout_join_ok_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/timeout_join_ok.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/timeout_join_ok.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn timeout_join_elapsed_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/timeout_join_elapsed.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/timeout_join_elapsed.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn timeout_elapsed_then_join_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/timeout_elapsed_then_join.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/timeout_elapsed_then_join.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn timeout_elapsed_twice_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/timeout_elapsed_twice.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/timeout_elapsed_twice.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn log_default_level_run() {
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/log_default_level.nova"))
.assert()
.success();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).replace("\r\n", "\n");
let levels: Vec<&str> = stderr
.lines()
.map(|l| l.split_once(' ').expect("line starts with a timestamp").1)
.collect();
assert_eq!(
levels,
vec!["INFO yes-info", "WARN yes-warn", "ERROR yes-error"]
);
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/log_default_level.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let stdout = String::from_utf8_lossy(&out.get_output().stdout).replace("\r\n", "\n");
assert_eq!(stdout, expected);
}
#[test]
fn log_init_with_threshold_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/log_init_with_threshold.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/log_init_with_threshold.nova"))
.assert()
.success()
.stdout(expected);
let stderr = String::from_utf8_lossy(&out.get_output().stderr).replace("\r\n", "\n");
let levels: Vec<&str> = stderr
.lines()
.map(|l| l.split_once(' ').expect("line starts with a timestamp").1)
.collect();
assert_eq!(levels, vec!["WARN kept-warn", "ERROR kept-error"]);
}
#[test]
fn log_reconfigure_after_logging_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/log_reconfigure_after_logging.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/log_reconfigure_after_logging.nova"))
.assert()
.success()
.stdout(expected);
let stderr = String::from_utf8_lossy(&out.get_output().stderr).replace("\r\n", "\n");
let levels: Vec<&str> = stderr
.lines()
.map(|l| l.split_once(' ').expect("line starts with a timestamp").1)
.collect();
assert_eq!(levels, vec!["INFO before", "ERROR after-kept"]);
}
#[test]
fn log_stdout_output_run() {
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/log_stdout_output.nova"))
.assert()
.success();
let output = out.get_output();
let stderr = String::from_utf8_lossy(&output.stderr).replace("\r\n", "\n");
assert_eq!(stderr, "", "stderr should be empty: {stderr}");
let stdout = String::from_utf8_lossy(&output.stdout).replace("\r\n", "\n");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 1, "stdout: {stdout}");
let (_, rest) = lines[0]
.split_once(' ')
.expect("line starts with a timestamp");
assert_eq!(rest, "INFO on-stdout");
}
#[test]
fn log_level_labels_run() {
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/log_level_labels.nova"))
.assert()
.success();
let output = out.get_output();
let stderr = String::from_utf8_lossy(&output.stderr).replace("\r\n", "\n");
assert_eq!(stderr, "", "stderr should be empty: {stderr}");
let stdout = String::from_utf8_lossy(&output.stdout).replace("\r\n", "\n");
let levels: Vec<&str> = stdout
.lines()
.map(|l| l.split_once(' ').expect("line starts with a timestamp").1)
.collect();
assert_eq!(
levels,
vec!["TRACE a", "DEBUG b", "INFO c", "WARN d", "ERROR e"]
);
}
#[test]
fn log_init_resets_to_default_run() {
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/log_init_resets_to_default.nova"))
.assert()
.success();
let output = out.get_output();
let stderr = String::from_utf8_lossy(&output.stderr).replace("\r\n", "\n");
let levels: Vec<&str> = stderr
.lines()
.map(|l| l.split_once(' ').expect("line starts with a timestamp").1)
.collect();
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/log_init_resets_to_default.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
let stdout = String::from_utf8_lossy(&output.stdout).replace("\r\n", "\n");
assert_eq!(stdout, expected);
assert_eq!(levels, vec!["INFO yes"]);
}
#[test]
fn fmt_int_pad_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fmt_int_pad.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fmt_int_pad.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn fmt_string_pad_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fmt_string_pad.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fmt_string_pad.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn fmt_float_fixed_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/fmt_float_fixed.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fmt_float_fixed.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn fmt_float_edge_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/fmt_float_edge.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/fmt_float_edge.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn sync_mutex_uncontended_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/sync_mutex_uncontended.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/sync_mutex_uncontended.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn sync_mutex_try_lock_fails_when_held_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/sync_mutex_try_lock_fails_when_held.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/sync_mutex_try_lock_fails_when_held.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn sync_mutex_release_is_idempotent_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/sync_mutex_release_is_idempotent.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/sync_mutex_release_is_idempotent.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn sync_mutex_two_tasks_serialise_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/sync_mutex_two_tasks_serialise.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/sync_mutex_two_tasks_serialise.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn sync_mutex_int_set_serialises_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/sync_mutex_int_set_serialises.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/sync_mutex_int_set_serialises.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn sync_mutex_stale_guard_cannot_steal_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/sync_mutex_stale_guard_cannot_steal.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/sync_mutex_stale_guard_cannot_steal.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn sync_mutex_stale_guard_cannot_write_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/sync_mutex_stale_guard_cannot_write.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/sync_mutex_stale_guard_cannot_write.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_uncontended_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/channel_uncontended.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_uncontended.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_full_refuses_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/channel_full_refuses.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_full_refuses.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_fifo_order_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/channel_fifo_order.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_fifo_order.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_close_refuses_send_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/channel_close_refuses_send.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_close_refuses_send.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_close_then_drain_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/channel_close_then_drain.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_close_then_drain.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_two_tasks_blocking_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/channel_two_tasks_blocking.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_two_tasks_blocking.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_recv_suspends_only_to_retry_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/channel_recv_suspends_only_to_retry.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_recv_suspends_only_to_retry.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_send_suspends_only_to_retry_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/channel_send_suspends_only_to_retry.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_send_suspends_only_to_retry.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_clamps_buffer_below_one_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/channel_clamps_buffer_below_one.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_clamps_buffer_below_one.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_enqueue_wraps_after_dequeue_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/channel_enqueue_wraps_after_dequeue.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_enqueue_wraps_after_dequeue.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn channel_send_refuses_when_closed_run() {
let expected = std::fs::read_to_string(
repo_root().join("tests/runtime/channel_send_refuses_when_closed.stdout"),
)
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/channel_send_refuses_when_closed.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn map_keys_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/map_keys.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/map_keys.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_stringify_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/json_stringify.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_stringify.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_stringify_escapes_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_stringify_escapes.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_stringify_escapes.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_parse_numbers_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_parse_numbers.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_parse_numbers.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_parse_strings_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_parse_strings.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_parse_strings.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_parse_values_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_parse_values.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_parse_values.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_round_trip_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_round_trip.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_round_trip.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_round_trip_under_gc_stress() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_round_trip.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/json_round_trip.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_stringify_escapes_under_gc_stress() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_stringify_escapes.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/json_stringify_escapes.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_parse_strings_under_gc_stress() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_parse_strings.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.env("NOVA_GC_STRESS", "1")
.arg("run")
.arg(repo_root().join("tests/runtime/json_parse_strings.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_traits_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/json_traits.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_traits.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_depth_leaf_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_depth_leaf.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_depth_leaf.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_depth_empty_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_depth_empty.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_depth_empty.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_render_deep_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_render_deep.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_render_deep.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_object_forged_map_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_object_forged_map.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_object_forged_map.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_stringify_cycle_panics_with_a_named_message() {
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_stringify_cycle.nova"))
.assert()
.failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: stringify: nesting too deep or cyclic value"),
"stderr: {stderr}"
);
assert!(
!stderr.contains("has overflowed its stack") && !stderr.contains("memory allocation of"),
"expected the guard, not a stack overflow or an allocator abort, stderr: {stderr}"
);
}
#[test]
fn json_render_guard_panics_with_a_named_message() {
let out = nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_render_guard.nova"))
.assert()
.failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("nova: panic: stringify: nesting too deep or cyclic value"),
"stderr: {stderr}"
);
assert!(
!stderr.contains("has overflowed its stack") && !stderr.contains("memory allocation of"),
"expected the guard, not a stack overflow or an allocator abort, stderr: {stderr}"
);
}
#[test]
fn json_render_guard_under_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/json_render_guard_under.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/json_render_guard_under.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn hash_diffusion_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/hash_diffusion.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/hash_diffusion.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn str_hash_seed_varies_across_processes() {
let dir = std::env::temp_dir().join(format!(
"nova-str-hash-seed-cross-process-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).expect("temp dir");
let file = dir.join("seed.nova");
std::fs::write(&file, "fn main() { println(\"${(\"alpha\").hash()}\") }\n").expect("write");
let exe = dir.join(format!("seed{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
let run = || {
let out = Command::new(&exe).assert().success();
String::from_utf8_lossy(&out.get_output().stdout)
.trim()
.to_string()
};
let first = run();
let second = run();
assert_ne!(
first, second,
"str_hash must differ across processes; both runs printed {first}"
);
}
const PHASE1_SRC: &str = r#"const CAP: Int = 64
const WANT: Int = 32
const BUDGET: Int = 4000
fn main() {
let mut counts = [0; CAP]
let mut target = -1
let mut i = 0
while i < BUDGET {
let k = "k${i}"
let b = k.hash() & (CAP - 1)
counts[b] = counts[b] + 1
if counts[b] >= WANT { target = b; break }
i = i + 1
}
if target < 0 {
println("found false")
return
}
let mut picked = [0; WANT]
let mut out = ""
let mut n = 0
let mut j = 0
while j < BUDGET {
let k = "k${j}"
if (k.hash() & (CAP - 1)) == target {
picked[n] = j
out = "${out}${j} "
n = n + 1
if n == WANT { break }
}
j = j + 1
}
// Self-check: bucket the emitted set with phase 2's own arithmetic. Under this
// process's seed every picked key shares `target`, so this is the whole
// set size; if it is not, the set being handed to phase 2 is not the set
// phase 1 claims to have found.
let mut own = [0; CAP]
let mut m = 0
while m < n {
let k = "k${picked[m]}"
own[k.hash() & (CAP - 1)] = own[k.hash() & (CAP - 1)] + 1
m = m + 1
}
let mut conc = 0
let mut q = 0
while q < CAP {
if own[q] > conc { conc = own[q] }
q = q + 1
}
println("found true")
println("count ${n}")
println("concentration ${conc}")
println("indices ${out}")
}
"#;
const PHASE2_TEMPLATE: &str = r#"const CAP: Int = 64
fn main() {
let idx = [__INDICES__]
let mut counts = [0; CAP]
let mut i = 0
while i < idx.len() {
let k = "k${idx[i]}"
let b = k.hash() & (CAP - 1)
counts[b] = counts[b] + 1
i = i + 1
}
let mut largest = 0
let mut j = 0
while j < CAP {
if counts[j] > largest { largest = counts[j] }
j = j + 1
}
println("keys ${idx.len()} largest ${largest}")
}
"#;
#[test]
fn hashdos_precomputed_key_set_does_not_survive_a_new_process() {
let dir = std::env::temp_dir().join(format!("nova-hashdos-resist-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
let build_and_capture = |name: &str, src: &str| -> String {
let file = dir.join(format!("{name}.nova"));
std::fs::write(&file, src).expect("write nova source");
let exe = dir.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe).assert().success();
String::from_utf8_lossy(&out.get_output().stdout)
.replace("\r\n", "\n")
.trim()
.to_string()
};
let found = build_and_capture("phase1", PHASE1_SRC);
assert!(
found.contains("found true"),
"phase 1 must find 32 keys in one bucket within its budget; it printed:\n{found}"
);
assert!(
found.contains("count 32"),
"phase 1 must report exactly the set size it was asked for; it printed:\n{found}"
);
assert!(
found.contains("concentration 32"),
"phase 1's self-check must confirm the 32 keys it emitted actually share one \
bucket under its own seed, not merely that it emitted 32 of them; it printed:\n{found}"
);
let indices: Vec<&str> = found
.lines()
.find_map(|l| l.strip_prefix("indices "))
.expect("phase 1 prints an `indices` line")
.split_whitespace()
.collect();
assert_eq!(
indices.len(),
32,
"phase 1's index line must carry the whole set; it printed:\n{found}"
);
let src = PHASE2_TEMPLATE.replace("__INDICES__", &indices.join(", "));
let spread = build_and_capture("phase2", &src);
assert!(
spread.starts_with("keys 32 largest "),
"phase 2 must report the set size and the largest bucket; it printed:\n{spread}"
);
let largest: usize = spread
.rsplit(' ')
.next()
.and_then(|n| n.parse().ok())
.expect("phase 2's last field is the largest bucket count");
assert!(
largest <= 10,
"a set precomputed under another seed must not still collide: largest bucket {largest} \
of 32 keys, bound 10. A dead seed yields 32. Phase 1 printed:\n{found}\n\
Phase 2 printed:\n{spread}"
);
}
const INT_PHASE1_SRC: &str = r#"const CAP: Int = 64
const WANT: Int = 32
const BUDGET: Int = 4000
fn main() {
let mut counts = [0; CAP]
let mut target = -1
let mut i = 0
while i < BUDGET {
let b = (i).hash() & (CAP - 1)
counts[b] = counts[b] + 1
if counts[b] >= WANT { target = b; break }
i = i + 1
}
if target < 0 {
println("found false")
return
}
let mut picked = [0; WANT]
let mut out = ""
let mut n = 0
let mut j = 0
while j < BUDGET {
if ((j).hash() & (CAP - 1)) == target {
picked[n] = j
out = "${out}${j} "
n = n + 1
if n == WANT { break }
}
j = j + 1
}
// Self-check: bucket the emitted set with phase 2's own arithmetic. Under
// this process's seed every picked key shares `target`, so this is the whole
// set size; if it is not, the set being handed to phase 2 is not the set
// phase 1 claims to have found.
let mut own = [0; CAP]
let mut m = 0
while m < n {
let k = picked[m]
own[k.hash() & (CAP - 1)] = own[k.hash() & (CAP - 1)] + 1
m = m + 1
}
let mut conc = 0
let mut q = 0
while q < CAP {
if own[q] > conc { conc = own[q] }
q = q + 1
}
println("found true")
println("count ${n}")
println("concentration ${conc}")
println("keys ${out}")
}
"#;
const INT_PHASE2_TEMPLATE: &str = r#"const CAP: Int = 64
fn main() {
let ks = [__KEYS__]
let mut counts = [0; CAP]
let mut i = 0
while i < ks.len() {
let k = ks[i]
let b = k.hash() & (CAP - 1)
counts[b] = counts[b] + 1
i = i + 1
}
let mut largest = 0
let mut j = 0
while j < CAP {
if counts[j] > largest { largest = counts[j] }
j = j + 1
}
println("keys ${ks.len()} largest ${largest}")
}
"#;
#[test]
fn hashdos_precomputed_int_key_set_does_not_survive_a_new_process() {
let dir = std::env::temp_dir().join(format!("nova-hashdos-int-resist-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
let build_and_capture = |name: &str, src: &str| -> String {
let file = dir.join(format!("{name}.nova"));
std::fs::write(&file, src).expect("write nova source");
let exe = dir.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(&file)
.arg("-o")
.arg(&exe)
.assert()
.success();
let out = Command::new(&exe).assert().success();
String::from_utf8_lossy(&out.get_output().stdout)
.replace("\r\n", "\n")
.trim()
.to_string()
};
let found = build_and_capture("int_phase1", INT_PHASE1_SRC);
assert!(
found.contains("found true"),
"phase 1 must find 32 Int keys in one bucket within its budget; it printed:\n{found}"
);
assert!(
found.contains("count 32"),
"phase 1 must report exactly the set size it was asked for; it printed:\n{found}"
);
assert!(
found.contains("concentration 32"),
"phase 1's self-check must confirm the 32 keys it emitted actually share one \
bucket under its own seed, not merely that it emitted 32 of them; it printed:\n{found}"
);
let keys: Vec<&str> = found
.lines()
.find_map(|l| l.strip_prefix("keys "))
.expect("phase 1 prints a `keys` line")
.split_whitespace()
.collect();
assert_eq!(
keys.len(),
32,
"phase 1's key line must carry the whole set; it printed:\n{found}"
);
let src = INT_PHASE2_TEMPLATE.replace("__KEYS__", &keys.join(", "));
let spread = build_and_capture("int_phase2", &src);
assert!(
spread.starts_with("keys 32 largest "),
"phase 2 must report the set size and the largest bucket; it printed:\n{spread}"
);
let largest: usize = spread
.rsplit(' ')
.next()
.and_then(|n| n.parse().ok())
.expect("phase 2's last field is the largest bucket count");
assert!(
largest <= 10,
"an Int key set precomputed under another seed must not still collide: largest \
bucket {largest} of 32 keys, bound 10. A dead seed yields 32. Phase 1 printed:\n\
{found}\nPhase 2 printed:\n{spread}"
);
}
#[test]
fn http_offsets_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/http_offsets.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/http_offsets.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn http_partial_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/http_partial.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/http_partial.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn http_malformed_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/http_malformed.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/http_malformed.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn http_limits_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/http_limits.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/http_limits.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn http_serialise_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/http_serialise.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/http_serialise.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn http_keepalive_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/http_keepalive.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/http_keepalive.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn http_server_dispatch_run() {
let expected =
std::fs::read_to_string(repo_root().join("tests/runtime/http_server_dispatch.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/http_server_dispatch.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn process_args_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/process_args.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.current_dir(repo_root())
.arg("run")
.arg("tests/runtime/process_args.nova")
.arg("--")
.args(["one", "two words", ""])
.assert()
.success()
.stdout(expected);
}
#[test]
fn process_args_without_a_separator_is_just_the_file() {
nova()
.current_dir(repo_root())
.arg("run")
.arg("tests/runtime/process_args.nova")
.assert()
.success()
.stdout("count: 1\n[tests/runtime/process_args.nova]\n");
}
#[test]
fn process_exit_run() {
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/process_exit.nova"))
.assert()
.code(3)
.stdout("partial");
}
#[test]
fn process_exit_build_standalone() {
let dir = std::env::temp_dir().join(format!("nova-build-tests-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
let exe = dir.join(format!("process_exit{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(repo_root().join("tests/runtime/process_exit.nova"))
.arg("-o")
.arg(&exe)
.assert()
.success();
Command::new(&exe).assert().code(3).stdout("partial");
let _ = std::fs::remove_file(&exe);
}
#[test]
fn bench_http_server_and_generator_agree() {
use std::io::{BufRead, BufReader};
use std::process::Stdio;
let generator = assert_cmd::cargo::cargo_bin("nova-bench-http");
let mut server = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova"))
.arg("run")
.arg(repo_root().join("docs/benchmarks/server.nova"))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn the benchmark server");
let stdout = server.stdout.take().expect("stdout was piped");
let first = BufReader::new(stdout)
.lines()
.next()
.and_then(|l| l.ok())
.unwrap_or_default();
let port = first
.rsplit(':')
.next()
.and_then(|p| p.trim().parse::<u16>().ok());
let port = match port {
Some(p) => p,
None => {
let _ = server.kill();
panic!("server did not print a parseable port line; got {first:?}");
}
};
let out = std::process::Command::new(generator)
.arg("--addr")
.arg(format!("127.0.0.1:{port}"))
.args(["--connections", "1", "--duration", "1", "--warmup", "0"])
.output()
.expect("run nova-bench-http");
let _ = server.kill();
let _ = server.wait();
let report = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
out.status.success(),
"generator failed: {}\nstderr: {}",
report,
String::from_utf8_lossy(&out.stderr)
);
let result = report
.lines()
.find(|l| l.starts_with("RESULT "))
.unwrap_or_else(|| panic!("no RESULT line in generator output: {report:?}"));
assert!(
result.contains(" errors=0 "),
"generator reported errors: {result}"
);
let requests: u64 = result
.split_whitespace()
.find_map(|f| f.strip_prefix("requests="))
.and_then(|v| v.parse().ok())
.unwrap_or(0);
assert!(requests >= 1, "no request completed: {result}");
let selftest = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova-bench-http"))
.arg("--self-test")
.args(["--connections", "1", "--duration", "1", "--warmup", "0"])
.output()
.expect("run nova-bench-http --self-test");
let selftest_report = String::from_utf8_lossy(&selftest.stdout).to_string();
assert!(
selftest.status.success(),
"--self-test failed: {}\nstderr: {}",
selftest_report,
String::from_utf8_lossy(&selftest.stderr)
);
let selftest_result = selftest_report
.lines()
.find(|l| l.starts_with("RESULT "))
.unwrap_or_else(|| {
panic!("no RESULT line in --self-test output: {selftest_report:?}");
});
assert!(
selftest_result.contains(" errors=0 "),
"--self-test reported errors: {selftest_result}"
);
let selftest_requests: u64 = selftest_result
.split_whitespace()
.find_map(|f| f.strip_prefix("requests="))
.and_then(|v| v.parse().ok())
.unwrap_or(0);
assert!(
selftest_requests >= 1,
"--self-test completed no request: {selftest_result}"
);
let dead_port = {
let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind an ephemeral port");
l.local_addr().expect("read the bound port").port()
};
let dead = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova-bench-http"))
.arg("--addr")
.arg(format!("127.0.0.1:{dead_port}"))
.args(["--connections", "1", "--duration", "1", "--warmup", "0"])
.output()
.expect("run nova-bench-http against a dead port");
assert!(
!dead.status.success(),
"nova-bench-http reported success against a port nothing is listening on \
(stdout: {}, stderr: {})",
String::from_utf8_lossy(&dead.stdout),
String::from_utf8_lossy(&dead.stderr)
);
}
#[test]
fn crypto_hashes_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/crypto_hashes.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/crypto_hashes.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn crypto_random_run() {
let expected = std::fs::read_to_string(repo_root().join("tests/runtime/crypto_random.stdout"))
.expect("expected-output fixture exists")
.replace("\r\n", "\n");
nova()
.arg("run")
.arg(repo_root().join("tests/runtime/crypto_random.nova"))
.assert()
.success()
.stdout(expected);
}
#[test]
fn json_api_example_serves_its_routes() {
use std::io::{BufRead, BufReader, Read, Write};
use std::process::Stdio;
let mut server = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova"))
.arg("run")
.arg(repo_root().join("examples/05-json-api/src/main.nova"))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn the json-api example");
let stdout = server.stdout.take().expect("stdout was piped");
let first = BufReader::new(stdout)
.lines()
.next()
.and_then(|l| l.ok())
.unwrap_or_default();
let port = match first
.rsplit(':')
.next()
.and_then(|p| p.trim().parse::<u16>().ok())
{
Some(p) => p,
None => {
let _ = server.kill();
let _ = server.wait();
panic!("the example did not print a parseable port line; got {first:?}");
}
};
fn content_length(head: &str) -> Option<usize> {
for line in head.split("\r\n") {
let Some((name, value)) = line.split_once(':') else {
continue;
};
if name.trim().eq_ignore_ascii_case("content-length") {
return value.trim().parse().ok();
}
}
None
}
let exchange = |method: &str, path: &str, body: &str| -> (u16, String) {
let mut wire = if body.is_empty() {
format!("{method} {path} HTTP/1.1\r\nhost: nova-json-api\r\n\r\n")
} else {
format!(
"{method} {path} HTTP/1.1\r\nhost: nova-json-api\r\n\
content-type: application/json\r\ncontent-length: {}\r\n\r\n",
body.len()
)
};
wire.push_str(body);
let mut sock = match std::net::TcpStream::connect(("127.0.0.1", port)) {
Ok(s) => s,
Err(e) => return (0, format!("connect failed: {e}")),
};
let limit = Some(std::time::Duration::from_secs(10));
if let Err(e) = sock.set_read_timeout(limit) {
return (0, format!("set_read_timeout failed: {e}"));
}
if let Err(e) = sock.set_write_timeout(limit) {
return (0, format!("set_write_timeout failed: {e}"));
}
if let Err(e) = sock.write_all(wire.as_bytes()) {
return (0, format!("write failed: {e}"));
}
let mut buf: Vec<u8> = Vec::new();
let mut chunk = [0u8; 4096];
let total = loop {
let framed = buf
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|i| i + 4)
.and_then(|end| {
let head = std::str::from_utf8(&buf[..end]).ok()?;
Some(end + content_length(head)?)
});
match framed {
Some(n) if buf.len() >= n => break n,
_ => {}
}
match sock.read(&mut chunk) {
Ok(0) => {
return (
0,
format!(
"peer closed mid-response after {} bytes: {:?}",
buf.len(),
String::from_utf8_lossy(&buf)
),
);
}
Ok(n) => buf.extend_from_slice(&chunk[..n]),
Err(e) => return (0, format!("read failed: {e}")),
}
};
let head_end = buf
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|i| i + 4)
.unwrap_or(0);
let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
let status = head
.lines()
.next()
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(0);
(
status,
String::from_utf8_lossy(&buf[head_end..total]).to_string(),
)
};
let empty = exchange("GET", "/users", "");
let created = exchange(
"POST",
"/users",
r#"{"name":"ada","email":"a@example.com"}"#,
);
let fetched = exchange("GET", "/users/1", "");
let bad_id = exchange("GET", "/users/zz", "");
let no_such = exchange("GET", "/users/99", "");
let unknown = exchange("GET", "/nope", "");
let wrong_prefix = exchange("GET", "/nope/1", "");
let escaped = exchange(
"POST",
"/users",
r#"{"name":"a\"b\\c","email":"q@example.com"}"#,
);
let listed = exchange("GET", "/users", "");
let _ = server.kill();
let _ = server.wait();
fn seen(r: &(u16, String)) -> (u16, &str) {
(r.0, r.1.as_str())
}
assert_eq!(seen(&empty), (200, "[]"), "GET /users on an empty store");
assert_eq!(
seen(&created),
(201, r#"{"id":1,"name":"ada","email":"a@example.com"}"#),
"POST /users"
);
assert_eq!(
seen(&fetched),
(200, r#"{"id":1,"name":"ada","email":"a@example.com"}"#),
"GET /users/1 over a later connection"
);
assert_eq!(
seen(&bad_id),
(400, r#"{"error":"id must be an integer"}"#),
"GET /users/zz"
);
assert_eq!(
seen(&no_such),
(404, r#"{"error":"no such user"}"#),
"GET /users/99"
);
assert_eq!(
seen(&unknown),
(404, r#"{"error":"not found"}"#),
"GET /nope"
);
assert_eq!(
seen(&wrong_prefix),
(404, r#"{"error":"not found"}"#),
"GET /nope/1: two segments, wrong resource -- not GET /users/:id"
);
assert_eq!(
seen(&escaped),
(201, r#"{"id":2,"name":"a\"b\\c","email":"q@example.com"}"#),
"POST /users with a quote and a backslash in the name"
);
assert_eq!(
seen(&listed),
(
200,
r#"[{"id":1,"name":"ada","email":"a@example.com"},{"id":2,"name":"a\"b\\c","email":"q@example.com"}]"#
),
"GET /users with two users: the separator and the ascending id walk"
);
}
static PORT_3000: std::sync::Mutex<()> = std::sync::Mutex::new(());
const HTTP03_ADDR: &str = "127.0.0.1:3000";
fn lock_port_3000() -> std::sync::MutexGuard<'static, ()> {
PORT_3000
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn http03_addr() -> std::net::SocketAddr {
HTTP03_ADDR.parse().expect("a valid socket address")
}
fn assert_port_3000_free() {
use std::time::Duration;
if let Ok(s) = std::net::TcpStream::connect_timeout(&http03_addr(), Duration::from_millis(500))
{
drop(s);
panic!("something is already listening on {HTTP03_ADDR}; free port 3000 and re-run");
}
if let Err(e) = std::net::TcpListener::bind("0.0.0.0:3000") {
panic!("cannot bind 0.0.0.0:3000 ({e}); free port 3000 and re-run");
}
}
fn describe_status(s: &std::process::ExitStatus) -> String {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = s.signal() {
return format!("killed by signal {sig}");
}
}
match s.code() {
Some(c) => format!("exit code {c} ({:#x})", c as u32),
None => format!("{s:?}"),
}
}
#[cfg(unix)]
fn send_termination_request(pid: u32) -> Result<(), String> {
extern "C" {
fn kill(pid: i32, sig: i32) -> i32;
}
let pid = i32::try_from(pid).map_err(|e| format!("pid {pid}: {e}"))?;
if pid <= 0 {
return Err(format!("refusing to signal pid {pid}"));
}
let rc = unsafe { kill(pid, 15) };
if rc == 0 {
Ok(())
} else {
Err(format!(
"kill(pid, SIGTERM) failed: {}",
std::io::Error::last_os_error()
))
}
}
#[cfg(windows)]
fn send_termination_request(pid: u32) -> Result<(), String> {
#[link(name = "kernel32")]
extern "system" {
fn GenerateConsoleCtrlEvent(ctrl_event: u32, process_group_id: u32) -> i32;
}
if pid == 0 {
return Err("refusing to signal process group 0".to_string());
}
let rc = unsafe { GenerateConsoleCtrlEvent(1, pid) };
if rc != 0 {
Ok(())
} else {
Err(format!(
"GenerateConsoleCtrlEvent(CTRL_BREAK, {pid}) failed: {}",
std::io::Error::last_os_error()
))
}
}
struct Http03Server {
child: std::process::Child,
stdout: Option<std::thread::JoinHandle<String>>,
stderr: Option<std::thread::JoinHandle<String>>,
}
impl Http03Server {
fn spawn() -> Self {
Self::spawn_with_env(&[])
}
fn spawn_with_env(env: &[(&str, &str)]) -> Self {
use std::io::Read;
use std::process::Stdio;
let mut cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova"));
cmd.arg("run")
.arg(repo_root().join("examples/03-http-server/src/main.nova"))
.envs(env.iter().copied())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0000_0200);
}
let mut child = cmd.spawn().expect("spawn nova run on the 03 example");
let mut out = child.stdout.take().expect("stdout was piped");
let mut err = child.stderr.take().expect("stderr was piped");
let stdout = std::thread::spawn(move || {
let mut s = String::new();
let _ = out.read_to_string(&mut s);
s
});
let stderr = std::thread::spawn(move || {
let mut s = String::new();
let _ = err.read_to_string(&mut s);
s
});
Http03Server {
child,
stdout: Some(stdout),
stderr: Some(stderr),
}
}
fn pid(&self) -> u32 {
self.child.id()
}
fn wait_until_accepting(&mut self) {
use std::time::{Duration, Instant};
let deadline = Instant::now() + Duration::from_secs(120);
loop {
if let Ok(s) =
std::net::TcpStream::connect_timeout(&http03_addr(), Duration::from_millis(500))
{
drop(s);
return;
}
if let Ok(Some(status)) = self.child.try_wait() {
let (out, err) = self.streams();
panic!(
"the example exited before listening: {}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
}
if Instant::now() >= deadline {
self.kill_and_panic("the example never accepted on 127.0.0.1:3000 within 120 s");
}
std::thread::sleep(Duration::from_millis(100));
}
}
fn wait_for_exit(&mut self, limit: std::time::Duration) -> Option<std::process::ExitStatus> {
let deadline = std::time::Instant::now() + limit;
loop {
match self.child.try_wait().expect("try_wait") {
Some(s) => return Some(s),
None if std::time::Instant::now() >= deadline => return None,
None => std::thread::sleep(std::time::Duration::from_millis(50)),
}
}
}
fn streams(&mut self) -> (String, String) {
let out = self
.stdout
.take()
.map(|h| h.join().unwrap_or_default())
.unwrap_or_default();
let err = self
.stderr
.take()
.map(|h| h.join().unwrap_or_default())
.unwrap_or_default();
(out, err)
}
fn kill_and_panic(&mut self, why: &str) -> ! {
let _ = self.child.kill();
let status = self.child.wait();
let (out, err) = self.streams();
panic!("{why}; force-killed: {status:?}; stdout={out:?} stderr={err:?}");
}
}
impl Drop for Http03Server {
fn drop(&mut self) {
if matches!(self.child.try_wait(), Ok(None)) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
}
struct Http03Conn {
sock: std::net::TcpStream,
pending: Vec<u8>,
}
struct Http03Response {
status: u16,
headers: std::collections::HashMap<String, String>,
body: String,
}
impl Http03Conn {
fn open() -> Self {
let sock = std::net::TcpStream::connect(HTTP03_ADDR).expect("connect to 127.0.0.1:3000");
let limit = Some(std::time::Duration::from_secs(10));
sock.set_read_timeout(limit).expect("set_read_timeout");
sock.set_write_timeout(limit).expect("set_write_timeout");
Http03Conn {
sock,
pending: Vec::new(),
}
}
fn send(&mut self, bytes: &str) {
use std::io::Write;
self.sock
.write_all(bytes.as_bytes())
.expect("write to the example");
}
fn response(&mut self) -> Result<Http03Response, String> {
use std::io::Read;
let mut chunk = [0u8; 4096];
loop {
if let Some(head_end) = self
.pending
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|i| i + 4)
{
let head = String::from_utf8_lossy(&self.pending[..head_end]).to_string();
let mut lines = head.split("\r\n");
let status = lines
.next()
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|s| s.parse::<u16>().ok())
.ok_or_else(|| format!("no status line in {head:?}"))?;
let mut headers = std::collections::HashMap::new();
for line in lines {
if let Some((name, value)) = line.split_once(':') {
headers.insert(name.trim().to_ascii_lowercase(), value.trim().to_string());
}
}
let len: usize = headers
.get("content-length")
.and_then(|v| v.parse().ok())
.ok_or_else(|| format!("no content-length in {head:?}"))?;
if self.pending.len() >= head_end + len {
let body = String::from_utf8_lossy(&self.pending[head_end..head_end + len])
.to_string();
self.pending.drain(..head_end + len);
return Ok(Http03Response {
status,
headers,
body,
});
}
}
match self.sock.read(&mut chunk) {
Ok(0) => {
return Err(format!(
"peer closed mid-response; had {:?}",
String::from_utf8_lossy(&self.pending)
))
}
Ok(n) => self.pending.extend_from_slice(&chunk[..n]),
Err(e) => {
return Err(format!(
"read failed: {e}; had {:?}",
String::from_utf8_lossy(&self.pending)
))
}
}
}
}
fn closed_by_server(&mut self) -> Result<(), String> {
use std::io::Read;
if !self.pending.is_empty() {
return Err(format!(
"unexpected bytes: {:?}",
String::from_utf8_lossy(&self.pending)
));
}
let mut chunk = [0u8; 64];
match self.sock.read(&mut chunk) {
Ok(0) => Ok(()),
Ok(n) => Err(format!(
"expected end of stream, got {:?}",
String::from_utf8_lossy(&chunk[..n])
)),
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::ConnectionReset | std::io::ErrorKind::ConnectionAborted
) =>
{
Ok(())
}
Err(e) => Err(format!("expected end of stream, got error {e}")),
}
}
}
fn http03_response(server: &mut Http03Server, conn: &mut Http03Conn, what: &str) -> Http03Response {
match conn.response() {
Ok(r) => r,
Err(e) => server.kill_and_panic(&format!("{what}: {e}")),
}
}
fn wait_until_refused() -> Result<(), String> {
use std::time::{Duration, Instant};
let deadline = Instant::now() + Duration::from_secs(15);
loop {
match std::net::TcpStream::connect_timeout(&http03_addr(), Duration::from_secs(3)) {
Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => return Ok(()),
Ok(s) => drop(s),
Err(_) => {}
}
if Instant::now() >= deadline {
return Err("the listener was still accepting 15 s after the signal".to_string());
}
std::thread::sleep(Duration::from_millis(50));
}
}
#[test]
fn http_server_example_serves_and_exits_cleanly() {
use std::time::Duration;
let _port = lock_port_3000();
assert_port_3000_free();
let mut server = Http03Server::spawn();
server.wait_until_accepting();
let mut k = Http03Conn::open();
k.send("GET / HTTP/1.1\r\nhost: x\r\n\r\n");
let hello = http03_response(&mut server, &mut k, "GET /");
assert_eq!(hello.status, 200);
assert_eq!(hello.body, "Hello from Nova!");
k.send("GET /health HTTP/1.1\r\nhost: x\r\n\r\n");
let health = http03_response(&mut server, &mut k, "GET /health");
assert_eq!(health.status, 200);
assert_eq!(
health.headers.get("content-type").map(String::as_str),
Some("application/json")
);
assert_eq!(
health.headers.get("content-length").map(String::as_str),
Some("15")
);
assert_eq!(health.body, r#"{"status":"ok"}"#);
k.send("POST / HTTP/1.1\r\nhost: x\r\ncontent-length: 3\r\n\r\nabc");
let post = http03_response(&mut server, &mut k, "POST / with a body");
assert_eq!(post.status, 404, "only GET routes ship");
k.send("GET / HTTP/1.1\r\nhost: x\r\n\r\n");
let again = http03_response(&mut server, &mut k, "GET / after a body");
assert_eq!(again.body, "Hello from Nova!");
let mut p = Http03Conn::open();
p.send("GET / HTTP/1.1\r\nhost: x\r\n\r\nGET /health HTTP/1.1\r\nhost: x\r\n\r\n");
let first = http03_response(&mut server, &mut p, "pipelined GET /");
let second = http03_response(&mut server, &mut p, "pipelined GET /health");
assert_eq!(first.body, "Hello from Nova!");
assert_eq!(second.body, r#"{"status":"ok"}"#);
let mut m = Http03Conn::open();
m.send("this is not http\r\n\r\n");
if let Err(e) = m.closed_by_server() {
server.kill_and_panic(&format!("malformed request: {e}"));
}
let mut f = Http03Conn::open();
f.send("GET / HTTP/1.1\r\nho");
std::thread::sleep(Duration::from_millis(300));
if let Err(e) = send_termination_request(server.pid()) {
server.kill_and_panic(&format!("could not deliver the signal: {e}"));
}
if let Err(e) = wait_until_refused() {
server.kill_and_panic(&e);
}
f.send("st: x\r\n\r\n");
let in_flight = http03_response(&mut server, &mut f, "the request straddling the signal");
assert_eq!(in_flight.status, 200);
assert_eq!(in_flight.body, "Hello from Nova!");
if let Err(e) = f.closed_by_server() {
server.kill_and_panic(&format!("after the in-flight answer: {e}"));
}
if let Err(e) = k.closed_by_server() {
server.kill_and_panic(&format!("idle keep-alive K: {e}"));
}
if let Err(e) = p.closed_by_server() {
server.kill_and_panic(&format!("idle pipelining P: {e}"));
}
let status = match server.wait_for_exit(Duration::from_secs(10)) {
Some(s) => s,
None => server.kill_and_panic("the example did not exit within 10 s of finishing"),
};
let (out, err) = server.streams();
assert_eq!(
status.code(),
Some(0),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
assert!(err.contains("listening on :3000"), "stderr: {err:?}");
}
#[test]
fn http_server_example_second_signal_forces_exit() {
use std::time::Duration;
let _port = lock_port_3000();
assert_port_3000_free();
let mut server = Http03Server::spawn();
server.wait_until_accepting();
let mut stalled = Http03Conn::open();
stalled.send("GET / HTTP/1.1\r\nho");
std::thread::sleep(Duration::from_millis(300));
if let Err(e) = send_termination_request(server.pid()) {
server.kill_and_panic(&format!("could not deliver the first signal: {e}"));
}
if let Err(e) = wait_until_refused() {
server.kill_and_panic(&e);
}
if let Err(e) = send_termination_request(server.pid()) {
server.kill_and_panic(&format!("could not deliver the second signal: {e}"));
}
let status = match server.wait_for_exit(Duration::from_secs(5)) {
Some(s) => s,
None => server.kill_and_panic("a second signal did not end the process within 5 s"),
};
let (out, err) = server.streams();
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert_eq!(
status.signal(),
Some(15),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
}
#[cfg(windows)]
{
assert_eq!(
status.code().map(|c| c as u32),
Some(0xC000_013A),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
}
drop(stalled);
}
#[test]
fn http_server_example_drops_a_stalled_request_at_its_deadline() {
use std::time::{Duration, Instant};
let _port = lock_port_3000();
assert_port_3000_free();
let mut server = Http03Server::spawn();
server.wait_until_accepting();
let mut stalled = Http03Conn::open();
stalled.send("GET / HTTP/1.1\r\nho");
std::thread::sleep(Duration::from_millis(300));
let signalled = Instant::now();
if let Err(e) = send_termination_request(server.pid()) {
server.kill_and_panic(&format!("could not deliver the signal: {e}"));
}
let status = match server.wait_for_exit(Duration::from_secs(20)) {
Some(s) => s,
None => server.kill_and_panic("the stalled request held the process past 20 s"),
};
let took = signalled.elapsed();
let (out, err) = server.streams();
assert_eq!(
status.code(),
Some(0),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
assert!(
took >= Duration::from_secs(8),
"exited {took:?} after the signal: the stalled request was not held to its deadline"
);
if let Err(e) = stalled.closed_by_server() {
panic!("the stalled connection was not closed by the server: {e}");
}
}
#[test]
fn http_server_example_bounds_the_drain_for_a_pipelining_client() {
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
let _port = lock_port_3000();
assert_port_3000_free();
let mut server = Http03Server::spawn();
server.wait_until_accepting();
let mut conn = Http03Conn::open();
let mut feed = conn.sock.try_clone().expect("clone the client socket");
let stop = Arc::new(AtomicBool::new(false));
let stop_feeding = Arc::clone(&stop);
let feeder = std::thread::spawn(move || {
let _ = feed.write_all(b"GET / HTTP/1.1\r\nho");
while !stop_feeding.load(Ordering::SeqCst) {
if feed
.write_all(b"st: x\r\n\r\nGET / HTTP/1.1\r\nho")
.is_err()
{
return;
}
std::thread::sleep(Duration::from_millis(20));
}
});
for i in 0..3 {
let r = http03_response(&mut server, &mut conn, &format!("pipelined response {i}"));
assert_eq!(r.body, "Hello from Nova!");
}
if let Err(e) = send_termination_request(server.pid()) {
stop.store(true, Ordering::SeqCst);
server.kill_and_panic(&format!("could not deliver the signal: {e}"));
}
if let Err(e) = wait_until_refused() {
stop.store(true, Ordering::SeqCst);
server.kill_and_panic(&e);
}
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if Instant::now() >= deadline {
stop.store(true, Ordering::SeqCst);
server.kill_and_panic("a pipelining client was still being served 5 s after the stop");
}
match conn.response() {
Ok(r) => assert_eq!(r.body, "Hello from Nova!"),
Err(_) => break,
}
}
stop.store(true, Ordering::SeqCst);
let _ = feeder.join();
let status = match server.wait_for_exit(Duration::from_secs(10)) {
Some(s) => s,
None => server.kill_and_panic("the example did not exit after the pipelining connection"),
};
let (out, err) = server.streams();
assert_eq!(
status.code(),
Some(0),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
}
#[test]
fn http_server_example_fails_fast_when_the_port_is_taken() {
use std::time::Duration;
let _port = lock_port_3000();
assert_port_3000_free();
let _holder = std::net::TcpListener::bind("0.0.0.0:3000").expect("hold port 3000");
let mut server = Http03Server::spawn();
let status = match server.wait_for_exit(Duration::from_secs(120)) {
Some(s) => s,
None => server.kill_and_panic("the example did not exit with port 3000 already taken"),
};
let (out, err) = server.streams();
assert!(
!status.success(),
"it must fail: {}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
assert!(
err.contains("called `unwrap` on an `Err` value"),
"stderr: {err:?}"
);
}
fn http03_stop_for_live_counts(server: &mut Http03Server) -> Vec<u64> {
use std::time::Duration;
if let Err(e) = send_termination_request(server.pid()) {
server.kill_and_panic(&format!("could not deliver the signal: {e}"));
}
let status = match server.wait_for_exit(Duration::from_secs(10)) {
Some(s) => s,
None => server.kill_and_panic("the example did not exit within 10 s of the signal"),
};
let (out, err) = server.streams();
assert_eq!(
status.code(),
Some(0),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
gc_live_object_counts(&err)
}
fn assert_live_set_bounded(live: &[u64], requests: usize, bound: u64) {
let max = live.iter().copied().max().unwrap_or(0);
assert!(
max < bound,
"the live set must stay bounded: a collection reported {max} live objects, \
the bound is {bound} (all {} collections: {live:?})",
live.len()
);
assert!(
live.len() >= 10,
"expected at least 10 collections over {requests} requests, saw {}: {live:?}",
live.len()
);
}
#[test]
fn http_server_example_keeps_a_bounded_live_set() {
const REQUESTS: usize = 3_000;
let _port = lock_port_3000();
assert_port_3000_free();
let mut server = Http03Server::spawn_with_env(&[("NOVA_GC_DEBUG", "1")]);
server.wait_until_accepting();
for i in 0..REQUESTS {
let mut conn = Http03Conn::open();
conn.send("GET / HTTP/1.1\r\nhost: x\r\n\r\n");
let r = http03_response(&mut server, &mut conn, &format!("request {i}"));
assert_eq!(r.status, 200, "request {i}");
}
let live = http03_stop_for_live_counts(&mut server);
assert_live_set_bounded(&live, REQUESTS, 1_000);
}
#[test]
fn http_server_example_keeps_a_bounded_live_set_under_concurrent_clients() {
const CLIENTS: usize = 8;
const PER_CLIENT: usize = 375;
let _port = lock_port_3000();
assert_port_3000_free();
let mut server = Http03Server::spawn_with_env(&[("NOVA_GC_DEBUG", "1")]);
server.wait_until_accepting();
let clients: Vec<_> = (0..CLIENTS)
.map(|c| {
std::thread::spawn(move || -> Result<(), String> {
for i in 0..PER_CLIENT {
let mut conn = Http03Conn::open();
conn.send("GET / HTTP/1.1\r\nhost: x\r\n\r\n");
let r = conn
.response()
.map_err(|e| format!("client {c}, request {i}: {e}"))?;
if r.status != 200 {
return Err(format!("client {c}, request {i}: status {}", r.status));
}
}
Ok(())
})
})
.collect();
for client in clients {
let outcome = client
.join()
.unwrap_or_else(|_| Err("a client thread panicked".to_string()));
if let Err(e) = outcome {
server.kill_and_panic(&e);
}
}
let live = http03_stop_for_live_counts(&mut server);
assert_live_set_bounded(&live, CLIENTS * PER_CLIENT, 2_000);
}
#[test]
fn a_server_that_never_calls_listen_keeps_the_default_signal_action() {
use std::io::{BufRead, BufReader, Read};
use std::process::Stdio;
use std::time::{Duration, Instant};
let mut cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin("nova"));
cmd.arg("run")
.arg(repo_root().join("docs/benchmarks/server.nova"))
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0000_0200);
}
let mut child = cmd
.spawn()
.expect("spawn nova run docs/benchmarks/server.nova");
let stdout = child.stdout.take().expect("stdout was piped");
let (tx, rx) = std::sync::mpsc::channel::<String>();
let out_thread = std::thread::spawn(move || {
let mut lines = BufReader::new(stdout).lines().map_while(Result::ok);
if let Some(first) = lines.next() {
let _ = tx.send(first);
}
lines.collect::<Vec<_>>().join("\n")
});
let mut stderr = child.stderr.take().expect("stderr was piped");
let err_thread = std::thread::spawn(move || {
let mut s = String::new();
let _ = stderr.read_to_string(&mut s);
s
});
let first = rx
.recv_timeout(Duration::from_secs(120))
.unwrap_or_default();
if !first.contains("listening on") {
let _ = child.kill();
let status = child.wait();
let err = err_thread.join().unwrap_or_default();
panic!("no ready line; got {first:?}; status {status:?}; stderr {err:?}");
}
std::thread::sleep(Duration::from_millis(500));
if let Err(e) = send_termination_request(child.id()) {
let _ = child.kill();
let _ = child.wait();
panic!("could not deliver the request: {e}");
}
let deadline = Instant::now() + Duration::from_secs(10);
let status = loop {
if let Some(s) = child.try_wait().expect("try_wait") {
break s;
}
if Instant::now() >= deadline {
let _ = child.kill();
let s = child.wait();
panic!("the request did not end the server within 10 s; force-killed: {s:?}");
}
std::thread::sleep(Duration::from_millis(50));
};
let out = out_thread.join().unwrap_or_default();
let err = err_thread.join().unwrap_or_default();
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert_eq!(
status.signal(),
Some(15),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
}
#[cfg(windows)]
{
assert_eq!(
status.code().map(|c| c as u32),
Some(0xC000_013A),
"{}; stdout={out:?} stderr={err:?}",
describe_status(&status)
);
}
}
fn todo_workdir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("nova-todo-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("todo work dir");
dir
}
fn todo_run(dir: &std::path::Path, args: &[&str]) -> std::process::Output {
nova()
.current_dir(dir)
.arg("run")
.arg(repo_root().join("examples/04-todo-cli/src/main.nova"))
.arg("--")
.args(args)
.output()
.expect("run nova")
}
fn todo_build(bin: &std::path::Path) -> std::path::PathBuf {
let exe = bin.join(format!("todo{}", std::env::consts::EXE_SUFFIX));
nova()
.arg("build")
.arg(repo_root().join("examples/04-todo-cli/src/main.nova"))
.arg("-o")
.arg(&exe)
.assert()
.success();
exe
}
fn todo_exe(exe: &std::path::Path, dir: &std::path::Path, args: &[&str]) -> std::process::Output {
std::process::Command::new(exe)
.current_dir(dir)
.args(args)
.output()
.expect("run the built todo")
}
fn stdout_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).replace("\r\n", "\n")
}
fn stderr_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).replace("\r\n", "\n")
}
fn todo_cycle(dir: &std::path::Path, run: &dyn Fn(&[&str]) -> std::process::Output) {
let step = |args: &[&str], want: &str| {
let out = run(args);
assert!(
out.status.success(),
"{args:?}: {:?}; stderr={}",
out.status,
stderr_of(&out)
);
assert_eq!(stdout_of(&out), want, "{args:?}");
};
step(&["add", "buy", "milk"], "added: 1\n");
step(&["add", "walk dog"], "added: 2\n");
step(&["list"], "[ ] 1: buy milk\n[ ] 2: walk dog\n");
step(&["done", "1"], "");
step(&["list"], "[x] 1: buy milk\n[ ] 2: walk dog\n");
let file = std::fs::read_to_string(dir.join("todos.json")).expect("todos.json written");
assert_eq!(
file,
r#"[{"id":1,"title":"buy milk","done":true},{"id":2,"title":"walk dog","done":false}]"#
);
}
#[test]
fn todo_cli_cycle_under_nova_run() {
let dir = todo_workdir("run-cycle");
todo_cycle(&dir, &|args| todo_run(&dir, args));
}
#[test]
fn todo_cli_cycle_as_a_built_executable() {
let bin = todo_workdir("built-bin");
let exe = todo_build(&bin);
let dir = todo_workdir("built-cycle");
todo_cycle(&dir, &|args| todo_exe(&exe, &dir, args));
}
fn todo_thai(dir: &std::path::Path, run: &dyn Fn(&[&str]) -> std::process::Output) {
let title = "ซื้อนม";
let added = run(&["add", title]);
assert!(added.status.success(), "add: {}", stderr_of(&added));
assert_eq!(stdout_of(&run(&["list"])), format!("[ ] 1: {title}\n"));
let file = std::fs::read_to_string(dir.join("todos.json")).expect("todos.json written");
assert_eq!(
file,
format!("[{{\"id\":1,\"title\":\"{title}\",\"done\":false}}]")
);
}
#[test]
fn todo_cli_keeps_a_thai_title_under_nova_run() {
let dir = todo_workdir("thai-run");
todo_thai(&dir, &|args| todo_run(&dir, args));
}
#[test]
fn todo_cli_keeps_a_thai_title_as_a_built_executable() {
let bin = todo_workdir("thai-bin");
let exe = todo_build(&bin);
let dir = todo_workdir("thai-built");
todo_thai(&dir, &|args| todo_exe(&exe, &dir, args));
}
#[test]
fn todo_cli_without_a_command_prints_usage_and_exits_1() {
let dir = todo_workdir("usage");
for args in [&[][..], &["frobnicate"][..]] {
let out = todo_run(&dir, args);
assert_eq!(out.status.code(), Some(1), "{args:?}");
assert_eq!(
stderr_of(&out),
"usage: todo {add <title> | list | done <id>}\n",
"{args:?}"
);
assert!(out.stdout.is_empty(), "{args:?}");
}
assert!(!dir.join("todos.json").exists());
}
#[test]
fn todo_cli_refuses_a_corrupt_todos_json_and_leaves_it_alone() {
let dir = todo_workdir("corrupt");
let cases: [(&str, &[&str]); 4] = [
("{not json", &["list"]),
("{not json", &["add", "x"]),
("{not json", &["done", "1"]),
(r#"[{"id":1}]"#, &["list"]),
];
for (content, args) in cases {
std::fs::write(dir.join("todos.json"), content).expect("write the corrupt file");
let out = todo_run(&dir, args);
assert_eq!(out.status.code(), Some(1), "{content:?} {args:?}");
let err = stderr_of(&out);
assert!(
err.starts_with("todo: cannot use todos.json: "),
"{content:?} {args:?}: {err:?}"
);
assert_eq!(
std::fs::read_to_string(dir.join("todos.json")).expect("still there"),
content,
"{content:?} {args:?} must leave the file alone"
);
}
}
#[test]
fn todo_cli_done_with_an_unknown_id_changes_nothing() {
let dir = todo_workdir("unknown-id");
assert!(todo_run(&dir, &["add", "a"]).status.success());
let before = std::fs::read(dir.join("todos.json")).expect("written");
for id in ["99", "x", ""] {
let out = todo_run(&dir, &["done", id]);
assert!(out.status.success(), "{id:?}: {}", stderr_of(&out));
assert!(out.stdout.is_empty(), "{id:?}");
assert_eq!(
std::fs::read(dir.join("todos.json")).expect("still there"),
before,
"{id:?}"
);
}
}
#[test]
fn check_prints_a_fix_as_a_help_line() {
let dir = std::env::temp_dir().join("nova-check-help-mutable");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
let main = dir.join("main.nova");
std::fs::write(
&main,
"fn main() {\n let x = 1\n x = 2\n println(\"${x}\")\n}\n",
)
.expect("write");
let assert = nova()
.env("NO_COLOR", "1")
.arg("check")
.arg(&main)
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("= help: make `x` mutable"),
"stderr: {stderr}"
);
assert!(
!stderr.contains("declare it as"),
"the fix replaces the note: {stderr}"
);
}
#[test]
fn check_prints_make_public_as_a_help_line() {
let dir = std::env::temp_dir().join("nova-check-help-public");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
std::fs::write(dir.join("lib.nova"), "fn hidden() -> Int {\n 1\n}\n").expect("write lib");
let main = dir.join("app.nova");
std::fs::write(
&main,
"import lib::{hidden}\n\nfn main() {\n println(\"${hidden()}\")\n}\n",
)
.expect("write main");
let assert = nova()
.env("NO_COLOR", "1")
.arg("check")
.arg(&main)
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(
stderr.contains("= help: make `hidden` public in `lib`"),
"stderr: {stderr}"
);
}
#[test]
fn example_05_geometry_runs() {
let home = std::env::temp_dir().join("nova-run-05-geometry-home");
let _ = std::fs::remove_dir_all(&home);
nova()
.current_dir(repo_root().join("examples/05-geometry"))
.env("NOVA_HOME", &home)
.arg("run")
.assert()
.success()
.stdout(
"area 3.141592653589793, perimeter 6.283185307179586\n\
area 6, perimeter 10\n\
total area 9.141592653589793\n",
);
}