1323 lines
34 KiB
Rust
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),
|
|
);
|
|
}
|