slovo/compiler/tests/standard_json_string_parsing_beta18.rs

464 lines
14 KiB
Rust

use std::{
env,
ffi::OsStr,
fs,
path::{Path, PathBuf},
process::{Command, Output},
sync::atomic::{AtomicUsize, Ordering},
};
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
#[test]
fn json_string_parser_lowers_to_private_runtime_helper() {
let fixture = write_fixture(
"lowering",
r#"
(module main)
(fn main () -> i32
(std.result.unwrap_ok (std.json.parse_string_value_result "\"slovo\""))
0)
"#,
);
let output = run_glagol([fixture.as_os_str()]);
assert_success("compile json string parser lowering", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("declare ptr @__glagol_json_parse_string_value_result(ptr)")
&& stdout.contains("call ptr @__glagol_json_parse_string_value_result(")
&& !stdout.contains("@std.json.parse_string_value_result"),
"JSON string parser LLVM shape drifted\nstdout:\n{}",
stdout
);
}
#[test]
fn test_runner_enforces_ascii_json_string_token_contract() {
let source = r#"
(module main)
(test "json string empty ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"\"")) ""))
(test "json string plain ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"slovo\"")) "slovo"))
(test "json string quote backslash slash ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"slo\\\"vo\\\\path\\/leaf\"")) "slo\"vo\\path/leaf"))
(test "json string newline tab escapes ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"line\\nnext\\tend\"")) "line\nnext\tend"))
(test "json string backspace formfeed roundtrip ok"
(= (std.json.quote_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"a\\b\\f\""))) "\"a\\b\\f\""))
(test "json string missing quotes err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "slovo")) 1))
(test "json string leading whitespace err"
(= (std.result.unwrap_err (std.json.parse_string_value_result " \"slovo\"")) 1))
(test "json string trailing whitespace err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slovo\" ")) 1))
(test "json string unterminated err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slovo")) 1))
(test "json string trailing bytes err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slovo\"x")) 1))
(test "json string raw quote err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slo\"vo\"")) 1))
(test "json string bad escape err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"bad\\x\"")) 1))
(test "json string unicode escape deferred err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"\\u0041\"")) 1))
(test "json string raw newline err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"line\nnext\"")) 1))
"#;
let fixture = write_fixture("test-runner", source);
let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
assert_success("run json string parser tests", &output);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
concat!(
"test \"json string empty ok\" ... ok\n",
"test \"json string plain ok\" ... ok\n",
"test \"json string quote backslash slash ok\" ... ok\n",
"test \"json string newline tab escapes ok\" ... ok\n",
"test \"json string backspace formfeed roundtrip ok\" ... ok\n",
"test \"json string missing quotes err\" ... ok\n",
"test \"json string leading whitespace err\" ... ok\n",
"test \"json string trailing whitespace err\" ... ok\n",
"test \"json string unterminated err\" ... ok\n",
"test \"json string trailing bytes err\" ... ok\n",
"test \"json string raw quote err\" ... ok\n",
"test \"json string bad escape err\" ... ok\n",
"test \"json string unicode escape deferred err\" ... ok\n",
"test \"json string raw newline err\" ... ok\n",
"14 test(s) passed\n",
),
"json string parser test runner stdout drifted"
);
}
#[test]
fn hosted_json_string_parser_smoke_when_toolchain_is_available() {
let fixture = write_fixture(
"runtime-smoke",
r#"
(module main)
(fn main () -> i32
(std.io.print_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"slovo\"")))
(std.io.print_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"slo\\\"vo\"")))
(std.io.print_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"path\\/leaf\"")))
(std.result.unwrap_err (std.json.parse_string_value_result "\"\\u0041\"")))
"#,
);
let binary = unique_path("json-string-parsing-beta18-bin");
let build = run_glagol([
OsStr::new("build"),
fixture.as_os_str(),
OsStr::new("-o"),
binary.as_os_str(),
]);
if !build.status.success() {
let stderr = String::from_utf8_lossy(&build.stderr);
assert!(
stderr.contains("ToolchainUnavailable"),
"json string parser build failed unexpectedly\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&build.stdout),
stderr
);
return;
}
let run = Command::new(&binary)
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
assert_eq!(
run.status.code(),
Some(1),
"json string parser binary exit code drifted\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(
String::from_utf8_lossy(&run.stdout),
"slovo\nslo\"vo\npath/leaf\n",
"json string parser binary stdout drifted"
);
assert!(
run.stderr.is_empty(),
"json string parser binary wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn hosted_json_string_runtime_rejects_raw_non_ascii_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!("skipping beta18 raw non-ASCII runtime smoke: set GLAGOL_CLANG or install clang");
return;
};
let c_source = write_c_fixture(
"raw-non-ascii",
r#"
#include <stdio.h>
#include <stdlib.h>
char *__glagol_json_parse_string_value_result(const char *text);
int main(void) {
const char token[] = { '"', (char)0xc3, (char)0xa9, '"', '\0' };
char *value = __glagol_json_parse_string_value_result(token);
if (value != NULL) {
free(value);
fputs("accepted raw non-ascii JSON string token\n", stderr);
return 1;
}
puts("raw-non-ascii rejected");
return 0;
}
"#,
);
let runtime = Path::new(env!("CARGO_MANIFEST_DIR")).join("../runtime/runtime.c");
let binary = unique_path("json-string-raw-non-ascii-beta18-bin");
let mut compile = Command::new(&clang);
compile.arg(runtime).arg(c_source).arg("-o").arg(&binary);
configure_clang_runtime_env(&mut compile, &clang);
let compile = compile
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
assert_success("clang beta18 raw non-ASCII runtime smoke", &compile);
let run = Command::new(&binary)
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
assert_eq!(
run.status.code(),
Some(0),
"raw non-ASCII runtime smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(
String::from_utf8_lossy(&run.stdout),
"raw-non-ascii rejected\n",
"raw non-ASCII runtime smoke stdout drifted"
);
assert!(
run.stderr.is_empty(),
"raw non-ASCII runtime smoke wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn json_string_parser_diagnostics_cover_promoted_name_and_shadowing() {
let arity = write_fixture(
"arity",
r#"
(module main)
(fn main () -> (result string i32)
(std.json.parse_string_value_result))
"#,
);
assert_rejected(
"std.json.parse_string_value_result arity",
run_glagol([arity.as_os_str()]),
"wrong number of arguments",
);
let type_mismatch = write_fixture(
"type",
r#"
(module main)
(fn main () -> (result string i32)
(std.json.parse_string_value_result 1))
"#,
);
assert_rejected(
"std.json.parse_string_value_result type",
run_glagol([type_mismatch.as_os_str()]),
"cannot call `std.json.parse_string_value_result` with argument of wrong type",
);
let source_shadow = write_fixture(
"source-shadow",
r#"
(module main)
(fn std.json.parse_string_value_result ((text string)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
);
assert_rejected(
"std.json.parse_string_value_result source shadow",
run_glagol([source_shadow.as_os_str()]),
"DuplicateFunction",
);
let helper_shadow = write_fixture(
"helper-shadow",
r#"
(module main)
(fn __glagol_json_parse_string_value_result ((text string)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
);
assert_rejected(
"__glagol_json_parse_string_value_result helper shadow",
run_glagol([helper_shadow.as_os_str()]),
"DuplicateFunction",
);
}
#[test]
fn deferred_json_parser_families_remain_unsupported_after_string_tokens() {
for name in [
"parse_object_result",
"parse_array_result",
"parse_value_result",
"tokenize_result",
"schema_validate_result",
"stream_parse_result",
] {
let fixture = write_fixture(
name,
&format!(
"(module main)\n\n(fn main () -> i32\n (std.json.{} \"[]\")\n 0)\n",
name
),
);
assert_rejected(
&format!("std.json.{name} deferred"),
run_glagol([fixture.as_os_str()]),
&format!("standard library call `std.json.{}` is not supported", name),
);
}
}
#[test]
fn unsupported_json_diagnostics_list_beta18_promoted_string_parser() {
let fixture = write_fixture(
"unsupported-guidance",
r#"
(module main)
(fn main () -> i32
(std.json.parse_value_result "null")
0)
"#,
);
let output = run_glagol([fixture.as_os_str()]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"unsupported JSON value parser unexpectedly compiled\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
stderr
);
assert!(
stderr.contains("standard library call `std.json.parse_value_result` is not supported"),
"unsupported JSON parser diagnostic drifted\nstderr:\n{}",
stderr
);
assert!(
stderr.contains("std.json.parse_string_value_result"),
"unsupported std guidance omitted promoted beta18 name\nstderr:\n{}",
stderr
);
}
fn write_fixture(name: &str, source: &str) -> PathBuf {
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
let dir = env::temp_dir().join(format!("glagol-json-string-beta18-{id}-{name}"));
fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
let path = dir.join("main.slo");
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
path
}
fn write_c_fixture(name: &str, source: &str) -> PathBuf {
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
let dir = env::temp_dir().join(format!("glagol-json-string-beta18-c-{id}-{name}"));
fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
let path = dir.join("main.c");
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
path
}
fn unique_path(name: &str) -> PathBuf {
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
env::temp_dir().join(format!("glagol-{name}-{id}{}", env::consts::EXE_SUFFIX))
}
fn find_clang() -> Option<PathBuf> {
if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(path));
}
let hermetic_clang = PathBuf::from("/tmp/glagol-clang-root/usr/bin/clang");
if hermetic_clang.is_file() {
return Some(hermetic_clang);
}
find_on_path("clang")
}
fn find_on_path(name: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
for dir in env::split_paths(&path) {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn configure_clang_runtime_env(command: &mut Command, clang: &Path) {
if !clang.starts_with("/tmp/glagol-clang-root") {
return;
}
let root = Path::new("/tmp/glagol-clang-root");
let lib_dir = root.join("usr/lib");
let lib64_dir = root.join("usr/lib64");
let include_dir = root.join("usr/include");
command.env("CPATH", include_dir);
command.env(
"LIBRARY_PATH",
format!("{}:{}", lib_dir.display(), lib64_dir.display()),
);
}
fn run_glagol<I, S>(args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::new(env!("CARGO_BIN_EXE_glagol"))
.args(args)
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
.output()
.expect("run glagol")
}
fn assert_success(context: &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{}",
context,
stdout,
stderr
);
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
fn assert_rejected(context: &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(),
"{} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
context,
stdout,
stderr
);
assert!(
stdout.is_empty(),
"{} rejected compile wrote stdout:\n{}",
context,
stdout
);
assert!(
stderr.contains(expected),
"{} diagnostic drifted; expected `{}`\nstderr:\n{}",
context,
expected,
stderr
);
}