use std::{ fs, path::PathBuf, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn unknown_function_is_rejected_without_panic() { assert_rejected( "unknown-function", r#" (module main) (fn main () -> i32 (missing 1)) "#, "UnknownFunction", ); } #[test] fn arity_mismatch_is_rejected_without_panic() { assert_rejected( "arity-mismatch", r#" (module main) (fn add ((a i32) (b i32)) -> i32 (+ a b)) (fn main () -> i32 (add 1)) "#, "ArityMismatch", ); } #[test] fn type_mismatch_is_rejected_without_panic() { assert_rejected( "type-mismatch", r#" (module main) (fn id ((value i32)) -> i32 value) (fn main () -> i32 (id true)) "#, "TypeMismatch", ); } #[test] fn unclosed_list_is_rejected_without_panic() { assert_rejected( "unclosed-list", r#" (module main) (fn main () -> i32 (+ 1 2) "#, "UnclosedList", ); } #[test] fn unknown_top_level_form_is_rejected_without_panic() { assert_rejected( "unknown-top-level-form", r#" (module main) (bogus top level) "#, "UnknownTopLevelForm", ); } #[test] fn checked_if_emits_llvm_without_panic() { let output = run_compiler( "checked-if", r#" (module main) (fn main () -> i32 (if true 1 0)) "#, ); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "compiler rejected checked if\nstdout:\n{}\nstderr:\n{}", stdout, stderr, ); assert!( stdout.contains("br i1") && stdout.contains(" phi i32 "), "checked if LLVM did not include branch and phi\nstdout:\n{}", stdout, ); assert!( !stderr.contains("panicked at") && !stderr.contains("thread 'main' panicked"), "compiler panicked for checked if\nstderr:\n{}", stderr, ); } #[test] fn top_level_test_does_not_break_compile_to_llvm() { let output = run_compiler( "top-level-test-compile", r#" (module main) (fn add ((a i32) (b i32)) -> i32 (+ a b)) (test "add works" (= (add 2 3) 5)) (fn main () -> i32 0) "#, ); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "compiler rejected top-level test fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr, ); assert!( stdout.contains("define i32 @add") && stdout.contains("define i32 @main"), "compiler did not emit expected functions\nstdout:\n{}", stdout, ); assert!( !stdout.contains("add works"), "compiler emitted test metadata into LLVM output\nstdout:\n{}", stdout, ); assert!( !stderr.contains("panicked at") && !stderr.contains("thread 'main' panicked"), "compiler panicked for top-level test fixture\nstderr:\n{}", stderr, ); } #[test] fn explicit_emit_llvm_matches_default_compile_mode() { let source = r#" (module main) (fn main () -> i32 0) "#; let default = run_compiler("default-compile-mode", source); let explicit = run_compiler_with_args("explicit-emit-llvm", source, ["--emit=llvm"]); assert_success_contains_needle("default compile", default, "define i32 @main"); assert_success_contains_needle("explicit --emit=llvm", explicit, "define i32 @main"); } #[test] fn output_file_receives_default_compile_output_without_stdout() { let fixture = write_fixture( "default-output-file", r#" (module main) (fn main () -> i32 0) "#, ); let output_path = temp_path("default-output-file", "ll"); let output = Command::new(compiler_path()) .arg("-o") .arg(&output_path) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_success_empty_stdout("default compile with -o", output); let output_file = fs::read_to_string(&output_path) .unwrap_or_else(|err| panic!("read `{}`: {}", output_path.display(), err)); assert!( output_file.contains("define i32 @main"), "output file did not contain LLVM IR\n{}", output_file, ); } #[test] fn output_file_receives_explicit_emit_llvm_output() { let fixture = write_fixture( "explicit-emit-output-file", r#" (module main) (fn main () -> i32 0) "#, ); let output_path = temp_path("explicit-emit-output-file", "ll"); let output = Command::new(compiler_path()) .arg("--emit=llvm") .arg(&fixture) .arg("-o") .arg(&output_path) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_success_empty_stdout("explicit --emit=llvm with -o", output); let output_file = fs::read_to_string(&output_path) .unwrap_or_else(|err| panic!("read `{}`: {}", output_path.display(), err)); assert!( output_file.contains("define i32 @main"), "output file did not contain LLVM IR\n{}", output_file, ); } #[test] fn manifest_records_successful_stdout_output() { let fixture = write_fixture( "manifest-stdout-success", r#" (module main) (fn main () -> i32 0) "#, ); let manifest_path = temp_path("manifest-stdout-success", "manifest.slo"); let output = Command::new(compiler_path()) .arg("--manifest") .arg(&manifest_path) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_success_contains_needle("manifest stdout success", output, "define i32 @main"); let manifest = read_manifest(&manifest_path); assert!( manifest.contains("(artifact-manifest\n") && manifest.contains(" (schema slovo.artifact-manifest)\n") && manifest.contains(" (version 1)\n") && manifest.contains(" (mode emit-llvm)\n") && manifest.contains(" (success true)\n") && manifest.contains(" (diagnostics-schema-version 1)\n") && manifest.contains(" (kind llvm-ir)\n") && manifest.contains(" (stdout \"") && manifest.contains("define i32 @main"), "manifest did not record successful stdout LLVM output\n{}", manifest, ); } #[test] fn manifest_records_output_file_path_for_o() { let fixture = write_fixture( "manifest-output-file", r#" (module main) (fn main () -> i32 0) "#, ); let output_path = temp_path("manifest-output-file", "ll"); let manifest_path = temp_path("manifest-output-file", "manifest.slo"); let output = Command::new(compiler_path()) .arg("--emit=llvm") .arg("-o") .arg(&output_path) .arg("--manifest") .arg(&manifest_path) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_success_empty_stdout("manifest with -o", output); let output_file = fs::read_to_string(&output_path) .unwrap_or_else(|err| panic!("read `{}`: {}", output_path.display(), err)); assert!( output_file.contains("define i32 @main"), "output file did not contain LLVM IR\n{}", output_file, ); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (success true)\n") && manifest.contains(" (kind llvm-ir)\n") && manifest.contains(&format!(" (path \"{}\")", output_path.display())) && manifest.contains(" (artifacts\n") && !manifest.contains(" (stdout \""), "manifest did not record -o path as the primary output\n{}", manifest, ); } #[test] fn manifest_records_source_diagnostic_failure() { let fixture = write_fixture( "manifest-diagnostic-failure", r#" (module main) (fn id ((value i32)) -> i32 value) (fn main () -> i32 (id true)) "#, ); let manifest_path = temp_path("manifest-diagnostic-failure", "manifest.slo"); let output = Command::new(compiler_path()) .arg("--manifest") .arg(&manifest_path) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("manifest diagnostic failure", &output, 1); assert!( stdout.is_empty(), "diagnostic failure wrote stdout:\n{}", stdout ); assert!( stderr.contains("(diagnostic\n") && stderr.contains(" (schema slovo.diagnostic)\n") && stderr.contains(" (version 1)\n") && stderr.contains(" (code TypeMismatch)\n"), "stderr did not contain v1 machine diagnostics\n{}", stderr, ); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (success false)\n") && manifest.contains(" (diagnostics-schema-version 1)\n") && manifest.contains(" (kind diagnostics)\n") && manifest.contains(" (stream stderr)\n") && manifest.contains("slovo.diagnostic") && manifest.contains("TypeMismatch"), "manifest did not record diagnostic failure\n{}", manifest, ); } #[test] fn manifest_records_run_tests_summary() { let fixture = write_fixture( "manifest-run-tests", r#" (module main) (fn add ((a i32) (b i32)) -> i32 (+ a b)) (test "add works" (= (add 2 3) 5)) "#, ); let manifest_path = temp_path("manifest-run-tests", "manifest.slo"); let output = Command::new(compiler_path()) .arg("--run-tests") .arg("--manifest") .arg(&manifest_path) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_success_contains( "manifest run tests", output, "test \"add works\" ... ok\n1 test(s) passed\n", ); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode run-tests)\n") && manifest.contains(" (test-report\n") && manifest.contains(" (total 1)\n") && manifest.contains(" (passed 1)\n") && manifest.contains(" (failed 0)\n") && manifest.contains(" (skipped 0)\n"), "manifest did not include run-test summary\n{}", manifest, ); } #[test] fn run_tests_filter_selects_skips_and_records_manifest_counts() { let fixture = write_fixture( "run-tests-filter", r#" (module main) (test "alpha first" true) (test "Alpha case" false) (test "beta second" true) "#, ); let manifest_path = temp_path("run-tests-filter", "manifest.slo"); let output = Command::new(compiler_path()) .arg("test") .arg(&fixture) .arg("--filter") .arg("alpha") .arg("--manifest") .arg(&manifest_path) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_success_contains( "filtered canonical test", output, "test \"alpha first\" ... ok\n\ test \"Alpha case\" ... skipped\n\ test \"beta second\" ... skipped\n\ 1 test(s) passed (total_discovered 3, selected 1, passed 1, failed 0, skipped 2, filter \"alpha\")\n", ); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (total 3)\n") && manifest.contains(" (total_discovered 3)\n") && manifest.contains(" (selected 1)\n") && manifest.contains(" (passed 1)\n") && manifest.contains(" (failed 0)\n") && manifest.contains(" (skipped 2)\n") && manifest.contains(" (filter \"alpha\")\n"), "manifest did not include filtered run-test summary\n{}", manifest, ); } #[test] fn legacy_run_tests_filter_zero_match_is_success() { let output = run_compiler_with_args( "legacy-run-tests-filter-zero-match", r#" (module main) (test "one" true) (test "two" true) "#, ["--run-tests", "--filter", "missing"], ); assert_success_contains( "legacy filtered zero match", output, "test \"one\" ... skipped\n\ test \"two\" ... skipped\n\ 0 test(s) passed (total_discovered 2, selected 0, passed 0, failed 0, skipped 2, filter \"missing\")\n", ); } #[test] fn run_tests_filter_selected_failure_records_counts() { let fixture = write_fixture( "run-tests-filter-selected-failure", r#" (module main) (test "passing skipped" true) (test "target failure" false) "#, ); let manifest_path = temp_path("run-tests-filter-selected-failure", "manifest.slo"); let output = Command::new(compiler_path()) .arg("test") .arg(&fixture) .arg("--filter") .arg("target") .arg("--manifest") .arg(&manifest_path) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("filtered selected failure", &output, 1); assert!( output.stdout.is_empty() && stderr.contains("TestFailed") && stderr.contains("test summary: total_discovered 2, selected 1, passed 0, failed 1, skipped 1, filter \"target\"") && !stderr.contains("passing skipped"), "filtered failure had unexpected output\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), stderr, ); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (success false)\n") && manifest.contains(" (total_discovered 2)\n") && manifest.contains(" (selected 1)\n") && manifest.contains(" (passed 0)\n") && manifest.contains(" (failed 1)\n") && manifest.contains(" (skipped 1)\n") && manifest.contains(" (filter \"target\")\n"), "manifest did not include filtered failure summary\n{}", manifest, ); } #[test] fn filter_cli_misuse_is_rejected() { let fixture = write_fixture( "filter-cli-misuse", "(module main)\n\n(fn main () -> i32\n 0)\n", ); let missing = Command::new(compiler_path()) .arg("test") .arg(&fixture) .arg("--filter") .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_cli_rejected("missing filter value", missing, "`--filter` requires"); let duplicate = Command::new(compiler_path()) .arg("test") .arg(&fixture) .arg("--filter") .arg("one") .arg("--filter") .arg("two") .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_cli_rejected( "duplicate filter", duplicate, "`--filter` was provided more than once", ); let wrong_mode = Command::new(compiler_path()) .arg("check") .arg(&fixture) .arg("--filter") .arg("one") .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_cli_rejected( "filter wrong mode", wrong_mode, "`--filter` is only supported", ); } #[test] fn unreadable_source_file_writes_failure_manifest_after_manifest_path_is_parsed() { let mut source_path = std::env::temp_dir(); let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed); source_path.push(format!( "glagol-strict-v0-{}-{}-manifest-missing.slo", std::process::id(), id )); let manifest_path = temp_path("manifest-unreadable-source", "manifest.slo"); let output = Command::new(compiler_path()) .arg("--manifest") .arg(&manifest_path) .arg(&source_path) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", source_path.display(), err)); assert_exit_code("unreadable source with manifest", &output, 1); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (success false)\n") && manifest.contains(" (mode emit-llvm)\n") && manifest.contains("InputReadFailed"), "input/read failure manifest mismatch\n{}", manifest ); } #[test] fn manifest_path_must_not_match_output_path() { let fixture = write_fixture( "manifest-output-same-path", r#" (module main) (fn main () -> i32 0) "#, ); let shared_path = temp_path("manifest-output-same-path", "out"); let output = Command::new(compiler_path()) .arg("-o") .arg(&shared_path) .arg("--manifest") .arg(&shared_path) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_cli_rejected( "manifest output same path", output, "output path and manifest path must be different", ); } #[test] fn manifest_path_must_not_alias_output_path() { let fixture = write_fixture( "manifest-output-alias-path", r#" (module main) (fn main () -> i32 0) "#, ); let output_path = temp_path("manifest-output-alias-path", "out"); let manifest_alias = output_path .parent() .expect("temp path has parent") .join(".") .join(output_path.file_name().expect("temp path has file name")); let output = Command::new(compiler_path()) .arg("-o") .arg(&output_path) .arg("--manifest") .arg(&manifest_alias) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_cli_rejected( "manifest output alias path", output, "output path and manifest path must be different", ); } #[test] fn output_file_receives_other_primary_mode_output() { let fixture = write_fixture( "format-output-file", r#" (module main) (fn main () -> i32 0) "#, ); let output_path = temp_path("format-output-file", "slo"); let output = Command::new(compiler_path()) .arg("--format") .arg("-o") .arg(&output_path) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_success_empty_stdout("--format with -o", output); let output_file = fs::read_to_string(&output_path) .unwrap_or_else(|err| panic!("read `{}`: {}", output_path.display(), err)); assert_eq!( output_file, "(module main)\n\n(fn main () -> i32\n 0)\n", "formatted output file mismatch", ); } #[test] fn top_level_tests_can_be_checked_and_run() { let source = r#" (module main) (fn add ((a i32) (b i32)) -> i32 (+ a b)) (test "add works" (= (add 2 3) 5)) "#; let checked = run_compiler_with_args("top-level-test-check", source, ["--check-tests"]); assert_success_contains( "check top-level tests", checked, "test \"add works\" ... checked\n1 test(s) checked\n", ); let run = run_compiler_with_args("top-level-test-run", source, ["--run-tests"]); assert_success_contains( "run top-level tests", run, "test \"add works\" ... ok\n1 test(s) passed\n", ); } #[test] fn failing_top_level_test_is_reported_without_panic() { assert_rejected_with_args( "failing-top-level-test", r#" (module main) (test "false is false" false) "#, ["--run-tests"], "TestFailed", ); } #[test] fn invalid_test_name_is_rejected_without_panic() { assert_rejected( "invalid-test-name", r#" (module main) (test not-a-string true) "#, "InvalidTestName", ); } #[test] fn invalid_decoded_test_names_are_rejected_without_panic() { let cases = [ ( "empty-test-name", r#" (module main) (test "" true) "#, ), ( "escaped-newline-test-name", r#" (module main) (test "bad\nname" true) "#, ), ( "escaped-quote-test-name", r#" (module main) (test "bad\"name" true) "#, ), ( "escaped-backslash-test-name", r#" (module main) (test "bad\\name" true) "#, ), ]; for (name, source) in cases { assert_rejected(name, source, "InvalidTestName"); } } #[test] fn duplicate_test_name_is_rejected_without_panic() { assert_rejected( "duplicate-test-name", r#" (module main) (test "same" true) (test "same" true) "#, "DuplicateTestName", ); } #[test] fn unsupported_escape_in_test_name_is_rejected_without_panic() { assert_rejected( "unsupported-escape-test-name", r#" (module main) (test "same" true) (test "sa\me" true) "#, "UnsupportedStringEscape", ); } #[test] fn non_bool_test_expression_is_rejected_without_panic() { assert_rejected( "non-bool-test-expression", r#" (module main) (test "not bool" 1) "#, "TestExpressionNotBool", ); } #[test] fn malformed_test_form_is_rejected_without_panic() { assert_rejected( "malformed-test-form", r#" (module main) (test "too many" true false) "#, "MalformedTestForm", ); } #[test] fn test_modes_are_mutually_exclusive_without_order_dependence() { let source = r#" (module main) (test "ok" true) "#; let cases = [ ("check-run-tests", ["--check-tests", "--run-tests"]), ("run-check-tests", ["--run-tests", "--check-tests"]), ("format-run-tests", ["--format", "--run-tests"]), ("emit-format", ["--emit=llvm", "--format"]), ("format-emit", ["--format", "--emit=llvm"]), ( "checked-lowering-check-tests", ["--inspect-lowering=checked", "--check-tests"], ), ]; for (name, args) in cases { let output = run_compiler_with_args(name, source, args); assert_cli_rejected(name, output, "mode flags are mutually exclusive"); } } #[test] fn string_literal_backend_gap_is_diagnostic_not_panic() { assert_rejected( "string-if", r#" (module main) (fn id ((value (ptr i32))) -> i32 0) "#, "UnsupportedBackendFeature", ); } #[test] fn integer_out_of_range_is_rejected_without_panic() { assert_rejected( "integer-out-of-range", r#" (module main) (fn main () -> i32 2147483648) "#, "IntegerOutOfRange", ); } #[test] fn unsupported_signature_type_is_rejected_without_panic() { assert_rejected( "unsupported-signature-type", r#" (module main) (fn id ((value (ptr i32))) -> i32 0) "#, "UnsupportedBackendFeature", ); } #[test] fn unsupported_unit_return_signature_is_rejected_without_panic() { assert_rejected( "unsupported-unit-return-signature", r#" (module main) (fn main () -> unit (print_i32 1)) "#, "UnsupportedUnitSignatureType", ); } #[test] fn unsupported_unit_parameter_signature_is_rejected_without_panic() { assert_rejected( "unsupported-unit-parameter-signature", r#" (module main) (fn ignore ((value unit)) -> i32 0) "#, "UnsupportedUnitSignatureType", ); } #[test] fn unsupported_unit_signatures_are_rejected_before_surface_lowering() { assert_rejected_with_args( "unsupported-unit-return-signature-surface-lowering", r#" (module main) (fn main () -> unit (print_i32 1)) "#, ["--inspect-lowering=surface"], "UnsupportedUnitSignatureType", ); assert_rejected_with_args( "unsupported-unit-parameter-signature-surface-lowering", r#" (module main) (fn ignore ((value unit)) -> i32 0) "#, ["--inspect-lowering=surface"], "UnsupportedUnitSignatureType", ); } #[test] fn help_is_a_successful_usage_request() { let output = run_compiler_raw(["--help"]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("help", &output, 0); assert!(stdout.is_empty(), "help wrote stdout:\n{}", stdout); assert!( stderr.contains("usage: glagol") && stderr.contains("--emit=llvm") && stderr.contains("--format") && stderr.contains("--print-tree") && stderr.contains("--inspect-lowering=surface") && stderr.contains("--inspect-lowering=checked") && stderr.contains("--check-tests") && stderr.contains("--run-tests") && stderr.contains("-o ") && stderr.contains("--manifest ") && stderr.contains("--version"), "help output did not describe the v0 CLI modes\nstderr:\n{}", stderr, ); } #[test] fn version_is_a_successful_metadata_request() { let output = run_compiler_raw(["--version"]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("version", &output, 0); assert_eq!( stdout, format!("{} {}\n", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), "version stdout mismatch", ); assert!(stderr.is_empty(), "version wrote stderr:\n{}", stderr); } #[test] fn missing_source_file_is_usage_error() { let output = run_compiler_raw([]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("missing source file", &output, 2); assert!( stdout.is_empty(), "missing source wrote stdout:\n{}", stdout ); assert!( stderr.contains("usage: glagol"), "missing source did not print usage\nstderr:\n{}", stderr, ); } #[test] fn missing_output_path_is_usage_error() { let output = run_compiler_raw(["-o"]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("missing output path", &output, 2); assert!( stdout.is_empty(), "missing output path wrote stdout:\n{}", stdout ); assert!( stderr.contains("`-o` requires a following path") && stderr.contains("usage: glagol"), "missing output path did not report usage failure\nstderr:\n{}", stderr, ); } #[test] fn unreadable_source_file_is_input_error() { let mut path = std::env::temp_dir(); let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed); path.push(format!( "glagol-strict-v0-{}-{}-missing.slo", std::process::id(), id )); let output = Command::new(compiler_path()) .arg(&path) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", path.display(), err)); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("unreadable source file", &output, 1); assert!( stdout.is_empty(), "unreadable source wrote stdout:\n{}", stdout ); assert!( stderr.contains("cannot read"), "unreadable source did not report input failure\nstderr:\n{}", stderr, ); } #[test] fn extra_argument_is_usage_error() { let fixture = write_fixture( "extra-argument", r#" (module main) (fn main () -> i32 0) "#, ); let output = Command::new(compiler_path()) .arg(&fixture) .arg("extra") .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_exit_code("extra argument", &output, 2); assert!( stdout.is_empty(), "extra argument wrote stdout:\n{}", stdout ); assert!( stderr.contains("unexpected argument `extra`") && stderr.contains("usage: glagol"), "extra argument did not report usage failure\nstderr:\n{}", stderr, ); } #[test] fn explicit_emit_llvm_extra_argument_is_usage_error() { let fixture = write_fixture( "explicit-emit-extra-argument", r#" (module main) (fn main () -> i32 0) "#, ); let output = Command::new(compiler_path()) .arg("--emit=llvm") .arg(&fixture) .arg("extra") .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)); assert_cli_rejected( "explicit emit extra argument", output, "unexpected argument `extra`", ); } fn assert_rejected(name: &str, source: &str, code: &str) { assert_rejected_with_args(name, source, [], code); } fn assert_rejected_with_args( name: &str, source: &str, args: [&str; N], code: &str, ) { let output = run_compiler_with_args(name, source, args); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "compiler unexpectedly accepted fixture `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr, ); assert_exit_code(name, &output, 1); assert!( stdout.is_empty(), "compiler emitted LLVM/stdout for rejected fixture `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr, ); assert!( stderr.contains(&format!("error[{}]", code)) || stderr.contains(&format!("(code {})", code)), "stderr for fixture `{}` did not contain diagnostic code `{}`\nstderr:\n{}", name, code, stderr, ); assert!( !stderr.contains("panicked at") && !stderr.contains("thread 'main' panicked"), "compiler panicked for fixture `{}`\nstderr:\n{}", name, stderr, ); } fn run_compiler(name: &str, source: &str) -> Output { run_compiler_with_args(name, source, []) } fn run_compiler_with_args(name: &str, source: &str, args: [&str; N]) -> Output { let fixture = write_fixture(name, source); Command::new(compiler_path()) .args(args) .arg(&fixture) .output() .unwrap_or_else(|err| panic!("run glagol on `{}`: {}", fixture.display(), err)) } fn run_compiler_raw(args: [&str; 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 assert_success_contains(name: &str, output: Output, expected: &str) { 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{}", name, stdout, stderr, ); assert_eq!(stdout, expected, "{} stdout mismatch", name); assert!(stderr.is_empty(), "{} wrote stderr:\n{}", name, stderr,); } fn assert_success_contains_needle(name: &str, output: Output, expected: &str) { 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{}", name, stdout, stderr, ); assert!( stdout.contains(expected), "{} stdout did not contain `{}`\nstdout:\n{}", name, expected, stdout, ); assert!(stderr.is_empty(), "{} wrote stderr:\n{}", name, stderr,); } fn assert_success_empty_stdout(name: &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{}", name, stdout, stderr, ); assert!(stdout.is_empty(), "{} wrote stdout:\n{}", name, stdout); assert!(stderr.is_empty(), "{} wrote stderr:\n{}", name, stderr); } fn assert_cli_rejected(name: &str, output: Output, expected: &str) { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "compiler unexpectedly accepted CLI fixture `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr, ); assert_exit_code(name, &output, 2); assert!( stdout.is_empty(), "compiler emitted stdout for rejected CLI fixture `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr, ); assert!( stderr.contains(expected), "stderr for CLI fixture `{}` did not contain `{}`\nstderr:\n{}", name, expected, stderr, ); assert!( !stderr.contains("panicked at") && !stderr.contains("thread 'main' panicked"), "compiler panicked for CLI fixture `{}`\nstderr:\n{}", name, stderr, ); } fn write_fixture(name: &str, source: &str) -> PathBuf { let path = temp_path(name, "slo"); fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); path } fn read_manifest(path: &PathBuf) -> String { fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err)) } fn temp_path(name: &str, extension: &str) -> PathBuf { let mut path = std::env::temp_dir(); let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed); path.push(format!( "glagol-strict-v0-{}-{}-{}.{}", std::process::id(), id, name, extension, )); path } fn assert_exit_code(name: &str, output: &Output, expected: i32) { assert_eq!( output.status.code(), Some(expected), "{} exit code mismatch\nstdout:\n{}\nstderr:\n{}", name, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr), ); }