464 lines
14 KiB
Rust
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
|
|
);
|
|
}
|