use std::{ fs, path::{Path, PathBuf}, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn local_canonical_fixture_is_formatter_stable() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = Path::new("../tests/canonical.fmt"); let expected = fs::read_to_string("../tests/canonical.fmt").expect("read canonical.fmt"); let output = run_formatter(compiler, fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); assert!( output.stderr.is_empty(), "formatter wrote stderr:\n{}", String::from_utf8_lossy(&output.stderr), ); } #[test] fn local_top_level_test_fixture_is_formatter_stable() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = Path::new("../tests/top-level-test.fmt"); let expected = fs::read_to_string("../tests/top-level-test.fmt").expect("read top-level-test.fmt"); let output = run_formatter(compiler, fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); assert!( output.stderr.is_empty(), "formatter wrote stderr:\n{}", String::from_utf8_lossy(&output.stderr), ); } #[test] fn local_v1_formatter_stability_fixture_is_formatter_stable() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = Path::new("../tests/formatter-stability-v1.fmt"); let expected = fs::read_to_string("../tests/formatter-stability-v1.fmt") .expect("read formatter-stability-v1.fmt"); let output = run_formatter(compiler, fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); assert!( output.stderr.is_empty(), "formatter wrote stderr:\n{}", String::from_utf8_lossy(&output.stderr), ); } #[test] fn local_comments_fixture_is_formatter_stable() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = Path::new("../tests/comments.slo"); let expected = fs::read_to_string("../tests/comments.slo").expect("read comments.slo"); let output = run_formatter(compiler, fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); assert!( output.stderr.is_empty(), "formatter wrote stderr:\n{}", String::from_utf8_lossy(&output.stderr), ); } #[test] fn formatter_canonicalizes_supported_syntax() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "supported-messy", r#"; status: formatter-canonical ; Scope: current strict supported syntax only. (module main) (fn add ( (a i32) (b i32)) -> i32 (+ a b)) (fn main() -> i32 (print_i32(add 20 22)) 0) "#, ); let expected = fs::read_to_string("../tests/canonical.fmt").expect("read canonical.fmt"); let output = run_formatter(compiler, &fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); } #[test] fn formatter_canonicalizes_struct_forms() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "struct", r#" (module main) (struct Point (x i32) (y i32)) (fn point_sum () -> i32 (+ (. (Point (x 20) (y 22)) x) (. (Point (x 20) (y 22)) y))) (fn main () -> i32 (point_sum)) "#, ); let expected = concat!( "(module main)\n", "\n", "(struct Point\n", " (x i32)\n", " (y i32))\n", "\n", "(fn point_sum () -> i32\n", " (+ (. (Point (x 20) (y 22)) x) (. (Point (x 20) (y 22)) y)))\n", "\n", "(fn main () -> i32\n", " (point_sum))\n", ); let output = run_formatter(compiler, &fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); } #[test] fn formatter_canonicalizes_top_level_tests() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "top-level-test", r#"; status: formatter-canonical ; Scope: promoted top-level test formatter contract. (module tests) (fn add ( (a i32) (b i32)) -> i32 (+ a b)) (test "add works" (= (add 2 3) 5)) (fn main() -> i32 0) "#, ); let expected = fs::read_to_string("../tests/top-level-test.fmt").expect("read top-level-test.fmt"); let output = run_formatter(compiler, &fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); } #[test] fn formatter_keeps_long_inline_forms_inline() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "long-inline", r#" (module main) (fn accept_many ( (a i32) (b i32) (c i32) (d i32) (e i32) (f i32) (g i32) (h i32) (i i32) (j i32)) -> i32 (+ a j)) (fn main () -> i32 (accept_many 100000001 100000002 100000003 100000004 100000005 100000006 100000007 100000008 100000009 100000010)) "#, ); let expected = concat!( "(module main)\n", "\n", "(fn accept_many ((a i32) (b i32) (c i32) (d i32) (e i32) (f i32) (g i32) (h i32) (i i32) (j i32)) -> i32\n", " (+ a j))\n", "\n", "(fn main () -> i32\n", " (accept_many 100000001 100000002 100000003 100000004 100000005 100000006 100000007 100000008 100000009 100000010))\n", ); let output = run_formatter(compiler, &fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), expected, ); } #[test] fn formatter_rejects_test_names_outside_v0_subset() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "invalid-test-name", r#" (module main) (test "bad\nname" true) "#, ); let output = run_formatter(compiler, &fixture); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "formatter unexpectedly accepted invalid test name\nstdout:\n{}\nstderr:\n{}", stdout, stderr, ); assert!( stdout.is_empty(), "formatter emitted stdout for rejected test name\nstdout:\n{}\nstderr:\n{}", stdout, stderr, ); assert!( stderr.contains("error[InvalidTestName]") || stderr.contains("(code InvalidTestName)"), "stderr did not contain InvalidTestName\nstderr:\n{}", stderr, ); } #[test] fn formatter_reports_unsupported_standard_library_calls() { let compiler = env!("CARGO_BIN_EXE_glagol"); let cases = [ ( "unsupported-std-call", r#" (module main) (fn main () -> i32 (std.io.print_unit 0) 0) "#, ), ( "unsupported-std-user-call", r#" (module main) (fn std.io.print_unit ((value i32)) -> i32 value) (fn main () -> i32 (std.io.print_unit 0)) "#, ), ]; for (name, source) in cases { let fixture = write_fixture(name, source); let output = run_formatter(compiler, &fixture); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "formatter unexpectedly accepted unsupported std call `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr, ); assert!( stdout.is_empty(), "formatter emitted stdout for unsupported std call `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr, ); assert!( stderr.contains("UnsupportedStandardLibraryCall"), "formatter stderr did not contain UnsupportedStandardLibraryCall for `{}`\nstderr:\n{}", name, stderr, ); } } #[test] fn formatter_preserves_full_line_comments_inside_function_bodies() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "body-comments", r#" (module main) (fn add ((a i32) (b i32)) -> i32 ; keep this in add (+ a b)) (fn main () -> i32 ; before effect (print_i32 (add 20 21)) ; before result (+ 20 22) ; after result ) "#, ); let output = run_formatter(compiler, &fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), concat!( "(module main)\n", "\n", "(fn add ((a i32) (b i32)) -> i32\n", " ; keep this in add\n", " (+ a b))\n", "\n", "(fn main () -> i32\n", " ; before effect\n", " (print_i32 (add 20 21))\n", " ; before result\n", " (+ 20 22)\n", " ; after result\n", ")\n", ), ); } #[test] fn formatter_preserves_full_line_comments_after_last_form() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "trailing-comments", r#" (module main) (fn main () -> i32 0) ; keep this file trailer ; and normalize indentation "#, ); let output = run_formatter(compiler, &fixture); assert_success(&output); assert_eq!( String::from_utf8(output.stdout).expect("formatter output is UTF-8"), concat!( "(module main)\n", "\n", "(fn main () -> i32\n", " 0)\n", "\n", "; keep this file trailer\n", "; and normalize indentation\n", ), ); } #[test] fn formatter_rejects_comments_inside_inline_expression_forms() { let compiler = env!("CARGO_BIN_EXE_glagol"); let fixture = write_fixture( "expression-comment", r#" (module main) (fn main () -> i32 (+ 20 ; cannot keep this while preserving inline calls 22)) "#, ); let output = run_formatter(compiler, &fixture); assert_formatter_comment_rejection(&output, "inline expression form"); } #[test] fn formatter_rejects_same_line_and_trailing_comments() { let compiler = env!("CARGO_BIN_EXE_glagol"); let cases = [ ( "same-line-module-comment", r#" (module main) ; unsupported trailing module comment (fn main () -> i32 0) "#, "same-line module comment", ), ( "same-line-expression-comment", r#" (module main) (fn main () -> i32 (+ 20 22) ; unsupported trailing expression comment ) "#, "same-line expression comment", ), ]; for (name, source, label) in cases { let fixture = write_fixture(name, source); let output = run_formatter(compiler, &fixture); assert_formatter_comment_rejection(&output, label); } } #[test] fn formatter_rejects_comments_in_headers_and_signatures() { let compiler = env!("CARGO_BIN_EXE_glagol"); let cases = [ ( "module-header-comment", r#" (module ; unsupported module header comment main) "#, "module header", ), ( "struct-signature-comment", r#" (module main) (struct ; unsupported struct signature comment Pair (left i32) (right i32)) "#, "struct signature", ), ( "function-signature-comment", r#" (module main) (fn add ((a i32) ; unsupported function signature comment (b i32)) -> i32 (+ a b)) "#, "function signature", ), ( "test-header-comment", r#" (module main) (test ; unsupported test header comment "addition works" true) "#, "test header", ), ]; for (name, source, label) in cases { let fixture = write_fixture(name, source); let output = run_formatter(compiler, &fixture); assert_formatter_comment_rejection(&output, label); } } fn run_formatter(compiler: &str, fixture: &Path) -> Output { Command::new(compiler) .arg("--format") .arg(fixture) .output() .unwrap_or_else(|err| panic!("run glagol --format on `{}`: {}", fixture.display(), err)) } fn assert_success(output: &Output) { assert!( output.status.success(), "formatter failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr), ); } fn assert_formatter_comment_rejection(output: &Output, label: &str) { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "formatter unexpectedly accepted unsupported comment position in {}\nstdout:\n{}\nstderr:\n{}", label, stdout, stderr, ); assert!( stdout.is_empty(), "formatter emitted stdout for rejected {}\nstdout:\n{}\nstderr:\n{}", label, stdout, stderr, ); assert!( stderr.contains("error[UnsupportedFormatterComment]"), "stderr did not contain human UnsupportedFormatterComment for {}\nstderr:\n{}", label, stderr, ); assert!( stderr.contains(" (schema slovo.diagnostic)\n") && stderr.contains(" (version 1)\n") && stderr.contains(" (code UnsupportedFormatterComment)\n") && stderr.contains(" (span\n"), "stderr did not contain structured UnsupportedFormatterComment diagnostic for {}\nstderr:\n{}", label, stderr, ); } fn write_fixture(name: &str, source: &str) -> PathBuf { let mut path = std::env::temp_dir(); let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed); path.push(format!( "glagol-formatter-{}-{}-{}.slo", std::process::id(), id, name )); fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); path }