slovo/compiler/tests/strict_v0_cli.rs
2026-05-22 08:38:43 +02:00

1323 lines
34 KiB
Rust

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 <path>")
&& stderr.contains("--manifest <path>")
&& 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<const N: usize>(
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<const N: usize>(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<const N: usize>(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),
);
}