use std::{ fs, path::{Path, PathBuf}, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn check_subcommand_accepts_supported_source_without_primary_output() { let fixture = write_fixture( "check", r#" (module main) (fn main () -> i32 0) "#, ); let output = run_glagol(["check".as_ref(), fixture.as_os_str()]); assert_success_stdout("check", output, ""); } #[test] fn check_subcommand_rejects_backend_boundary_without_primary_output() { let fixture = write_fixture( "check-backend-gap", r#" (module main) (fn id ((value (ptr i32))) -> i32 0) "#, ); let output = run_glagol(["check".as_ref(), fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("check backend boundary", &output, 1); assert!( stdout.is_empty(), "failed check wrote primary stdout:\n{}", stdout ); assert!( stderr.contains("UnsupportedBackendFeature"), "backend boundary diagnostic mismatch:\n{}", stderr ); } #[test] fn fmt_subcommand_matches_legacy_format_flag() { let fixture = write_fixture( "fmt-alias", r#" (module main) (fn main () -> i32 0) "#, ); let legacy = run_glagol(["--format".as_ref(), fixture.as_os_str()]); let alias = run_glagol(["fmt".as_ref(), fixture.as_os_str()]); assert_success("legacy --format", &legacy); assert_success("fmt", &alias); assert_eq!(alias.stdout, legacy.stdout, "fmt alias output mismatch"); } #[test] fn test_subcommand_matches_legacy_run_tests_flag() { let fixture = write_fixture( "test-alias", r#" (module main) (test "true" true) "#, ); let legacy = run_glagol(["--run-tests".as_ref(), fixture.as_os_str()]); let alias = run_glagol(["test".as_ref(), fixture.as_os_str()]); assert_success("legacy --run-tests", &legacy); assert_success("test", &alias); assert_eq!(alias.stdout, legacy.stdout, "test alias output mismatch"); } #[test] fn json_diagnostics_emit_one_object_per_line_without_human_preamble() { let fixture = write_fixture( "json-type-mismatch", r#" (module main) (fn id ((value i32)) -> i32 value) (fn main () -> i32 (id true)) "#, ); let output = run_glagol(["--json-diagnostics".as_ref(), fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("json diagnostics", &output, 1); assert!( stdout.is_empty(), "json diagnostic failure wrote stdout:\n{}", stdout ); assert!( stderr .lines() .all(|line| line.starts_with('{') && line.ends_with('}')), "stderr contained non-JSON diagnostic text:\n{}", stderr ); assert!( stderr.contains(r#""schema":"slovo.diagnostic""#) && stderr.contains(r#""version":1"#) && stderr.contains(r#""code":"TypeMismatch""#) && stderr.contains(r#""file":"#) && stderr.contains(r#""span":{"byte_start":"#), "json diagnostic did not contain expected fields:\n{}", stderr ); } #[test] fn json_diagnostics_cover_parse_error() { let fixture = write_fixture("json-parse", "(module main"); let output = run_glagol(["--json-diagnostics".as_ref(), fixture.as_os_str()]); assert_exit_code("json parse error", &output, 1); assert_json_diagnostic("json parse error", &output, "UnclosedList"); } #[test] fn json_diagnostics_cover_lowering_error() { let fixture = write_fixture( "json-lowering", r#" (module main) (bogus top level) "#, ); let output = run_glagol(["--json-diagnostics".as_ref(), fixture.as_os_str()]); assert_exit_code("json lowering error", &output, 1); assert_json_diagnostic("json lowering error", &output, "UnknownTopLevelForm"); } #[test] fn json_diagnostics_cover_formatter_error() { let fixture = write_fixture( "json-formatter", r#" (module main) ; trailing comments are outside the formatter subset (fn main () -> i32 0) "#, ); let output = run_glagol([ "--json-diagnostics".as_ref(), "fmt".as_ref(), fixture.as_os_str(), ]); assert_exit_code("json formatter error", &output, 1); assert_json_diagnostic( "json formatter error", &output, "UnsupportedFormatterComment", ); } #[test] fn json_diagnostics_cover_failed_test() { let fixture = write_fixture( "json-failed-test", r#" (module main) (test "false" false) "#, ); let output = run_glagol([ "--json-diagnostics".as_ref(), "test".as_ref(), fixture.as_os_str(), ]); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("json failed test", &output, 1); assert_json_diagnostic("json failed test", &output, "TestFailed"); assert!( stderr.contains(r#""expected":"true""#) && stderr.contains(r#""found":"false""#), "failed-test JSON diagnostic did not include expected/found:\n{}", stderr ); } #[test] fn json_diagnostics_cover_backend_boundary_through_check() { let fixture = write_fixture( "json-check-backend-gap", r#" (module main) (fn id ((value (ptr i32))) -> i32 0) "#, ); let output = run_glagol([ "--json-diagnostics".as_ref(), "check".as_ref(), fixture.as_os_str(), ]); assert_exit_code("json check backend boundary", &output, 1); assert_json_diagnostic( "json check backend boundary", &output, "UnsupportedBackendFeature", ); } #[test] fn json_diagnostics_cover_usage_error_with_manifest() { let manifest_path = temp_path("json-usage", "manifest.slo"); let output = run_glagol([ "--json-diagnostics".as_ref(), "--manifest".as_ref(), manifest_path.as_os_str(), ]); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("json usage error", &output, 2); assert_json_diagnostic("json usage error", &output, "UsageError"); assert!( stderr.contains(r#""file":null"#) && stderr.contains(r#""span":null"#), "usage JSON diagnostic should not claim a source span:\n{}", stderr ); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode usage-error)\n") && manifest.contains(" (success false)\n") && manifest.contains(" (diagnostics-encoding json)\n") && manifest.contains("UsageError"), "usage JSON manifest mismatch:\n{}", manifest ); } #[test] fn json_diagnostics_cover_hosted_build_missing_clang() { let fixture = write_fixture( "json-missing-clang", r#" (module main) (fn main () -> i32 0) "#, ); let output_path = temp_path("json-missing-clang", "bin"); let manifest_path = temp_path("json-missing-clang", "manifest.slo"); let missing_clang = temp_path("json-missing-clang", "not-a-clang"); let output = Command::new(compiler_path()) .arg("--json-diagnostics") .arg("build") .arg(&fixture) .arg("-o") .arg(&output_path) .arg("--manifest") .arg(&manifest_path) .env("GLAGOL_CLANG", &missing_clang) .output() .unwrap_or_else(|err| panic!("run glagol build: {}", err)); assert_exit_code("json missing clang", &output, 3); assert_json_diagnostic("json missing clang", &output, "ToolchainUnavailable"); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode build)\n") && manifest.contains(" (success false)\n") && manifest.contains(" (diagnostics-encoding json)\n") && manifest.contains("ToolchainUnavailable"), "missing clang JSON manifest mismatch:\n{}", manifest ); } #[test] fn manifest_records_successful_check_fmt_and_test_v1_1_commands() { let source = r#" (module main) (fn main () -> i32 0) "#; let fixture = write_fixture("manifest-success-commands", source); let check_manifest = temp_path("manifest-check-success", "manifest.slo"); let check = run_glagol([ "check".as_ref(), "--manifest".as_ref(), check_manifest.as_os_str(), fixture.as_os_str(), ]); assert_success_stdout("manifest check success", check, ""); let manifest = read_manifest(&check_manifest); assert!( manifest.contains(" (mode check)\n") && manifest.contains(" (success true)\n") && manifest.contains(" (kind no-output)\n") && !manifest.contains(" (stdout "), "check success manifest mismatch:\n{}", manifest ); let fmt_manifest = temp_path("manifest-fmt-success", "manifest.slo"); let fmt = run_glagol([ "fmt".as_ref(), "--manifest".as_ref(), fmt_manifest.as_os_str(), fixture.as_os_str(), ]); assert_success("manifest fmt success", &fmt); let manifest = read_manifest(&fmt_manifest); assert!( manifest.contains(" (mode format)\n") && manifest.contains(" (success true)\n") && manifest.contains(" (kind formatted-source)\n") && manifest.contains("(fn main () -> i32"), "fmt success manifest mismatch:\n{}", manifest ); let test_fixture = write_fixture( "manifest-test-success", r#" (module main) (test "true" true) "#, ); let test_manifest = temp_path("manifest-test-success", "manifest.slo"); let test = run_glagol([ "test".as_ref(), "--manifest".as_ref(), test_manifest.as_os_str(), test_fixture.as_os_str(), ]); assert_success("manifest test success", &test); let manifest = read_manifest(&test_manifest); assert_manifest_test_report(&manifest, 1, 1, 0, 0); assert!( manifest.contains(" (mode test)\n") && manifest.contains(" (success true)\n"), "test success manifest mismatch:\n{}", manifest ); } #[test] fn manifest_records_failed_test_counts_after_execution_begins() { let fixture = write_fixture( "manifest-failed-test", r#" (module main) (test "passes" true) (test "fails" false) "#, ); let manifest_path = temp_path("manifest-failed-test", "manifest.slo"); let output = run_glagol([ "test".as_ref(), "--manifest".as_ref(), manifest_path.as_os_str(), fixture.as_os_str(), ]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("manifest failed test", &output, 1); assert!( stdout.is_empty(), "failed test invocation wrote primary stdout:\n{}", stdout ); assert!( stderr.contains("TestFailed"), "failed test diagnostic mismatch:\n{}", stderr ); let manifest = read_manifest(&manifest_path); assert_manifest_test_report(&manifest, 2, 1, 1, 0); assert!( manifest.contains(" (mode test)\n") && manifest.contains(" (success false)\n") && manifest.contains("TestFailed"), "failed test manifest mismatch:\n{}", manifest ); } #[test] fn manifest_records_source_diagnostic_failure() { let fixture = write_fixture( "manifest-source-failure", r#" (module main) (fn id ((value i32)) -> i32 value) (fn main () -> i32 (id true)) "#, ); let manifest_path = temp_path("manifest-source-failure", "manifest.slo"); let output = run_glagol([ "check".as_ref(), "--manifest".as_ref(), manifest_path.as_os_str(), fixture.as_os_str(), ]); assert_exit_code("manifest source failure", &output, 1); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode check)\n") && manifest.contains(" (success false)\n") && manifest.contains(" (kind diagnostics)\n") && manifest.contains("TypeMismatch"), "source diagnostic manifest mismatch:\n{}", manifest ); } #[test] fn usage_failure_writes_manifest_when_manifest_path_was_parsed() { let fixture = write_fixture( "usage-manifest", r#" (module main) (fn main () -> i32 0) "#, ); let manifest_path = temp_path("usage-manifest", "manifest.slo"); let output = run_glagol([ "build".as_ref(), "--manifest".as_ref(), manifest_path.as_os_str(), fixture.as_os_str(), ]); assert_exit_code("build missing output", &output, 2); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (success false)\n") && manifest.contains(" (mode usage-error)\n") && manifest.contains("`build` requires `-o `"), "usage failure manifest mismatch:\n{}", manifest ); } #[test] fn build_reports_missing_clang_as_toolchain_failure_with_manifest() { let fixture = write_fixture( "missing-clang", r#" (module main) (fn main () -> i32 0) "#, ); let output_path = temp_path("missing-clang", "bin"); let manifest_path = temp_path("missing-clang", "manifest.slo"); let missing_clang = temp_path("missing-clang", "not-a-clang"); let output = Command::new(compiler_path()) .arg("build") .arg(&fixture) .arg("-o") .arg(&output_path) .arg("--manifest") .arg(&manifest_path) .env("GLAGOL_CLANG", &missing_clang) .output() .unwrap_or_else(|err| panic!("run glagol build: {}", err)); assert_exit_code("missing clang", &output, 3); assert!( !output_path.exists(), "build left output behind after missing clang" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("cannot find Clang executable"), "missing clang diagnostic mismatch:\n{}", stderr ); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode build)\n") && manifest.contains(" (success false)\n") && manifest.contains("ToolchainUnavailable"), "missing clang manifest mismatch:\n{}", manifest ); } #[test] #[ignore = "requires hosted clang and system linker"] fn hosted_build_smoke_builds_and_runs_promoted_v1_1_examples() { let Some(clang) = find_clang() else { eprintln!("skipping hosted build smoke: set GLAGOL_CLANG or install clang"); return; }; let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); let cases = [ ("add", "add.slo", Some(0), "42\n"), ("while", "while.slo", Some(4), ""), ("struct-value-flow", "struct-value-flow.slo", Some(42), ""), ("array-value-flow", "array-value-flow.slo", Some(11), ""), ( "option-result-payload", "option-result-payload.slo", Some(0), "", ), ( "option-result-match", "option-result-match.slo", Some(0), "", ), ( "string-print", "string-print.slo", Some(0), "hello\nline\nquote\"slash\\tab\t\n", ), ]; for (name, fixture_name, expected_status, expected_stdout) in cases { let fixture = manifest.join("../examples").join(fixture_name); let output_path = temp_path(&format!("hosted-build-{}", name), "bin"); let manifest_path = temp_path(&format!("hosted-build-{}", name), "manifest.slo"); let mut build_command = Command::new(compiler_path()); build_command .arg("build") .arg(&fixture) .arg("-o") .arg(&output_path) .arg("--manifest") .arg(&manifest_path) .env("GLAGOL_CLANG", &clang); configure_clang_runtime_env(&mut build_command, &clang); let build = build_command .output() .unwrap_or_else(|err| panic!("run glagol build for `{}`: {}", name, err)); assert_success(&format!("hosted build {}", name), &build); let build_manifest = read_manifest(&manifest_path); assert!( build_manifest.contains(" (mode build)\n") && build_manifest.contains(" (success true)\n") && build_manifest.contains(" (kind native-executable)\n") && build_manifest.contains(" (hosted-build\n"), "hosted build manifest mismatch for `{}`:\n{}", name, build_manifest ); let run = Command::new(&output_path) .output() .unwrap_or_else(|err| panic!("run `{}`: {}", output_path.display(), err)); assert_eq!( run.status.code(), expected_status, "{} exit status mismatch\nstdout:\n{}\nstderr:\n{}", name, String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr) ); assert_eq!( String::from_utf8_lossy(&run.stdout), expected_stdout, "{} stdout mismatch", name ); } } fn run_glagol(args: [&std::ffi::OsStr; N]) -> Output { Command::new(compiler_path()) .args(args) .output() .unwrap_or_else(|err| panic!("run glagol: {}", err)) } fn compiler_path() -> &'static str { env!("CARGO_BIN_EXE_glagol") } fn write_fixture(name: &str, source: &str) -> PathBuf { let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed); let path = temp_path(&format!("{}-{}", name, id), "slo"); fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); path } fn temp_path(name: &str, extension: &str) -> PathBuf { let mut path = std::env::temp_dir(); path.push(format!( "glagol-v1-1-{}-{}.{}", std::process::id(), name, extension )); path } fn read_manifest(path: &Path) -> String { fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err)) } fn assert_success(context: &str, output: &Output) { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "{} failed\nstdout:\n{}\nstderr:\n{}", context, stdout, stderr ); assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr); } fn assert_success_stdout(context: &str, output: Output, expected: &str) { assert_success(context, &output); let stdout = String::from_utf8(output.stdout).expect("stdout is UTF-8"); assert_eq!(stdout, expected, "{} stdout mismatch", context); } fn assert_exit_code(context: &str, output: &Output, expected: i32) { assert_eq!( output.status.code(), Some(expected), "{} exit code mismatch\nstdout:\n{}\nstderr:\n{}", context, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } fn assert_json_diagnostic(context: &str, output: &Output, expected_code: &str) { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stdout.is_empty(), "{} wrote stdout on diagnostic failure:\n{}", context, stdout ); assert!( !stderr.trim().is_empty(), "{} did not write a JSON diagnostic", context ); assert!( stderr .lines() .all(|line| line.starts_with('{') && line.ends_with('}')), "{} stderr contained non-JSON diagnostic text:\n{}", context, stderr ); assert!( stderr.contains(r#""schema":"slovo.diagnostic""#) && stderr.contains(r#""version":1"#) && stderr.contains(&format!(r#""code":"{}""#, expected_code)) && stderr.contains(r#""severity":"error""#) && stderr.contains(r#""file":"#) && stderr.contains(r#""span":"#), "{} JSON diagnostic did not contain expected fields:\n{}", context, stderr ); } fn assert_manifest_test_report( manifest: &str, total: usize, passed: usize, failed: usize, skipped: usize, ) { assert!( manifest.contains(" (test-report\n") && manifest.contains(&format!(" (total {})\n", total)) && manifest.contains(&format!(" (passed {})\n", passed)) && manifest.contains(&format!(" (failed {})\n", failed)) && manifest.contains(&format!(" (skipped {})\n", skipped)), "manifest test report mismatch:\n{}", manifest ); } fn find_clang() -> Option { if let Some(path) = std::env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) { return Some(PathBuf::from(path)); } let hermetic_clang = PathBuf::from("/tmp/glagol-clang-root/usr/bin/clang"); if hermetic_clang.is_file() { return Some(hermetic_clang); } find_on_path("clang") } fn find_on_path(program: &str) -> Option { let path = std::env::var_os("PATH")?; std::env::split_paths(&path) .map(|dir| dir.join(program)) .find(|candidate| candidate.is_file()) } fn configure_clang_runtime_env(command: &mut Command, clang: &Path) { if !clang.starts_with("/tmp/glagol-clang-root") { return; } let root = Path::new("/tmp/glagol-clang-root"); let lib64 = root.join("usr/lib64"); let lib = root.join("usr/lib"); let mut paths = vec![lib64, lib]; if let Some(existing) = std::env::var_os("LD_LIBRARY_PATH") { paths.extend(std::env::split_paths(&existing)); } let joined = std::env::join_paths(paths).expect("join LD_LIBRARY_PATH"); command.env("LD_LIBRARY_PATH", joined); }