slovo/compiler/tests/formatter.rs

688 lines
16 KiB
Rust

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_rejects_reserved_generic_collection_syntax() {
let compiler = env!("CARGO_BIN_EXE_glagol");
let cases = [
(
"generic-function",
r#"
(module main)
(fn id (type_params T) ((value T)) -> T
value)
"#,
"UnsupportedGenericFunction",
),
(
"generic-type-alias",
r#"
(module main)
(type VecOf (type_params T) (vec T))
"#,
"UnsupportedGenericTypeAlias",
),
(
"generic-type-parameter",
r#"
(module main)
(fn main () -> i32
(let xs (vec T) (std.vec.i32.empty))
0)
"#,
"UnsupportedGenericTypeParameter",
),
(
"map-type",
r#"
(module main)
(fn main () -> (map string i32)
0)
"#,
"UnsupportedMapType",
),
(
"set-type",
r#"
(module main)
(fn main () -> (set string)
0)
"#,
"UnsupportedSetType",
),
(
"generic-std-call",
r#"
(module main)
(fn main () -> i32
(std.vec.empty i32)
0)
"#,
"UnsupportedGenericStandardLibraryCall",
),
];
for (name, source, code) 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 reserved generic syntax `{}`\nstdout:\n{}\nstderr:\n{}",
name,
stdout,
stderr,
);
assert!(
stdout.is_empty(),
"formatter emitted stdout for reserved generic syntax `{}`\nstdout:\n{}\nstderr:\n{}",
name,
stdout,
stderr,
);
assert!(
stderr.contains(code),
"formatter stderr did not contain {} for `{}`\nstderr:\n{}",
code,
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
}