1623 lines
57 KiB
Rust
1623 lines
57 KiB
Rust
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process::{Command, Output},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
static NEXT_PROJECT_ID: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
const UNSAFE_HEADS: &[&str] = &[
|
|
"alloc",
|
|
"dealloc",
|
|
"load",
|
|
"store",
|
|
"ptr_add",
|
|
"unchecked_index",
|
|
"reinterpret",
|
|
"ffi_call",
|
|
];
|
|
|
|
#[test]
|
|
fn check_accepts_basic_project_root_and_manifest_path() {
|
|
let project = write_project(
|
|
"basic",
|
|
&[(
|
|
"math",
|
|
"(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n (+ value 1))\n",
|
|
)],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n (add_one 41))\n",
|
|
);
|
|
|
|
let root = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout("project root check", root, "");
|
|
|
|
let manifest = project.join("slovo.toml");
|
|
let manifest_path = run_glagol(["check".as_ref(), manifest.as_os_str()]);
|
|
assert_success_stdout("manifest path check", manifest_path, "");
|
|
}
|
|
|
|
#[test]
|
|
fn project_check_preserves_enum_payload_layout_metadata() {
|
|
let project = write_project(
|
|
"project-enum-payload",
|
|
&[],
|
|
"(module main)\n\n\
|
|
(enum Reading\n Missing\n (Value string))\n\n\
|
|
(fn same ((left Reading) (right Reading)) -> bool\n (= left right))\n\n\
|
|
(fn main () -> i32\n (if (same (Reading.Value \"hello\") (Reading.Value \"hello\")) 0 1))\n",
|
|
);
|
|
|
|
let output = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
|
|
assert_success_stdout("project enum payload check", output, "");
|
|
}
|
|
|
|
#[test]
|
|
fn project_imports_exported_direct_string_enum_values_across_modules() {
|
|
let project = write_project(
|
|
"project-direct-string-enum-imports",
|
|
&[(
|
|
"readings",
|
|
"(module readings (export LabelReading make text same))\n\n\
|
|
(enum LabelReading\n Missing\n (Value string))\n\n\
|
|
(fn make ((value string)) -> LabelReading\n (LabelReading.Value value))\n\n\
|
|
(fn text ((reading LabelReading)) -> string\n (match reading\n ((LabelReading.Missing)\n \"\")\n ((LabelReading.Value payload)\n payload)))\n\n\
|
|
(fn same ((left LabelReading) (right LabelReading)) -> bool\n (= left right))\n",
|
|
)],
|
|
"(module main)\n\n\
|
|
(import readings (LabelReading make text same))\n\n\
|
|
(fn pass ((reading LabelReading)) -> LabelReading\n reading)\n\n\
|
|
(fn main () -> i32\n (if (same (pass (make \"hello\")) (LabelReading.Value \"hello\")) 0 1))\n\n\
|
|
(test \"project string enum import match\"\n (= (text (pass (make \"hello\"))) \"hello\"))\n\n\
|
|
(test \"project string enum import equality\"\n (same (pass (LabelReading.Value \"hello\")) (make \"hello\")))\n",
|
|
);
|
|
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout("project string enum import check", check, "");
|
|
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout(
|
|
"project string enum import test",
|
|
test,
|
|
"test \"project string enum import match\" ... ok\n\
|
|
test \"project string enum import equality\" ... ok\n\
|
|
2 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_imports_exported_enum_values_across_modules() {
|
|
let project = write_project(
|
|
"project-enum-imports",
|
|
&[(
|
|
"readings",
|
|
"(module readings (export Reading make score same))\n\n\
|
|
(enum Reading\n Missing\n (Value i32))\n\n\
|
|
(fn make ((value i32)) -> Reading\n (Reading.Value value))\n\n\
|
|
(fn score ((reading Reading)) -> i32\n (match reading\n ((Reading.Missing)\n 0)\n ((Reading.Value payload)\n payload)))\n\n\
|
|
(fn same ((left Reading) (right Reading)) -> bool\n (= left right))\n",
|
|
)],
|
|
"(module main)\n\n\
|
|
(import readings (Reading make score same))\n\n\
|
|
(fn pass ((reading Reading)) -> Reading\n reading)\n\n\
|
|
(fn main () -> i32\n (let reading Reading (pass (make 41)))\n (score reading))\n\n\
|
|
(test \"project enum import match\"\n (let reading Reading (pass (make 41)))\n (= (score reading) 41))\n\n\
|
|
(test \"project enum import equality\"\n (same (pass (Reading.Value 41)) (make 41)))\n",
|
|
);
|
|
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout("project enum import check", check, "");
|
|
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout(
|
|
"project enum import test",
|
|
test,
|
|
"test \"project enum import match\" ... ok\n\
|
|
test \"project enum import equality\" ... ok\n\
|
|
2 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_imports_direct_scalar_and_string_enum_payloads_across_modules() {
|
|
let project = write_project(
|
|
"project-enum-direct-scalars",
|
|
&[(
|
|
"readings",
|
|
"(module readings (export WideReading LabelReading make_wide make_label wide_code label_text same_wide same_label))\n\n\
|
|
(enum WideReading\n Missing\n (Value i64))\n\n\
|
|
(enum LabelReading\n Missing\n (Value string))\n\n\
|
|
(fn make_wide ((value i64)) -> WideReading\n (WideReading.Value value))\n\n\
|
|
(fn make_label ((value string)) -> LabelReading\n (LabelReading.Value value))\n\n\
|
|
(fn wide_code ((reading WideReading)) -> i64\n (match reading\n ((WideReading.Missing)\n 0i64)\n ((WideReading.Value payload)\n payload)))\n\n\
|
|
(fn label_text ((reading LabelReading)) -> string\n (match reading\n ((LabelReading.Missing)\n \"\")\n ((LabelReading.Value payload)\n payload)))\n\n\
|
|
(fn same_wide ((left WideReading) (right WideReading)) -> bool\n (= left right))\n\n\
|
|
(fn same_label ((left LabelReading) (right LabelReading)) -> bool\n (= left right))\n",
|
|
)],
|
|
"(module main)\n\n\
|
|
(import readings (WideReading LabelReading make_wide make_label wide_code label_text same_wide same_label))\n\n\
|
|
(fn pass_wide ((reading WideReading)) -> WideReading\n reading)\n\n\
|
|
(fn pass_label ((reading LabelReading)) -> LabelReading\n reading)\n\n\
|
|
(fn main () -> i32\n (if (= (wide_code (pass_wide (make_wide 41i64))) 41i64)\n 41\n 0))\n\n\
|
|
(test \"project direct scalar enum import wide match\"\n (= (wide_code (pass_wide (make_wide 41i64))) 41i64))\n\n\
|
|
(test \"project direct scalar enum import wide equality\"\n (same_wide (pass_wide (WideReading.Value 41i64)) (make_wide 41i64)))\n\n\
|
|
(test \"project direct scalar enum import string match\"\n (= (label_text (pass_label (make_label \"hi\"))) \"hi\"))\n\n\
|
|
(test \"project direct scalar enum import string equality\"\n (same_label (pass_label (LabelReading.Value \"hi\")) (make_label \"hi\")))\n",
|
|
);
|
|
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout("project direct scalar enum import check", check, "");
|
|
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout(
|
|
"project direct scalar enum import test",
|
|
test,
|
|
"test \"project direct scalar enum import wide match\" ... ok\n\
|
|
test \"project direct scalar enum import wide equality\" ... ok\n\
|
|
test \"project direct scalar enum import string match\" ... ok\n\
|
|
test \"project direct scalar enum import string equality\" ... ok\n\
|
|
4 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn std_layout_local_math_project_uses_only_explicit_local_imports() {
|
|
let project =
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-math");
|
|
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout("std layout local math check", check, "");
|
|
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout(
|
|
"std layout local math test",
|
|
test,
|
|
"test \"local math i32 helpers\" ... ok\n\
|
|
test \"local math i64 helpers\" ... ok\n\
|
|
test \"local math f64 helpers\" ... ok\n\
|
|
test \"explicit local math import i32 helpers\" ... ok\n\
|
|
test \"explicit local math import i64 helpers\" ... ok\n\
|
|
test \"explicit local math import f64 helpers\" ... ok\n\
|
|
test \"explicit local math import all helpers\" ... ok\n\
|
|
7 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn std_layout_local_result_project_uses_only_explicit_local_imports() {
|
|
let project =
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-result");
|
|
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout("std layout local result check", check, "");
|
|
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
assert_success_stdout(
|
|
"std layout local result test",
|
|
test,
|
|
"test \"explicit local result i32 wrappers\" ... ok\n\
|
|
test \"explicit local result err wrappers\" ... ok\n\
|
|
test \"explicit local result unwrap_or i32\" ... ok\n\
|
|
test \"explicit local result ok_or_none i32 ok\" ... ok\n\
|
|
test \"explicit local result ok_or_none i32 none\" ... ok\n\
|
|
test \"explicit local result u32 wrappers\" ... ok\n\
|
|
test \"explicit local result unwrap_or u32\" ... ok\n\
|
|
test \"explicit local result ok_or_none u32 ok\" ... ok\n\
|
|
test \"explicit local result ok_or_none u32 none\" ... ok\n\
|
|
test \"explicit local result unwrap_or i64\" ... ok\n\
|
|
test \"explicit local result ok_or_none i64 ok\" ... ok\n\
|
|
test \"explicit local result ok_or_none i64 none\" ... ok\n\
|
|
test \"explicit local result u64 wrappers\" ... ok\n\
|
|
test \"explicit local result unwrap_or u64\" ... ok\n\
|
|
test \"explicit local result ok_or_none u64 ok\" ... ok\n\
|
|
test \"explicit local result ok_or_none u64 none\" ... ok\n\
|
|
test \"explicit local result unwrap_or string\" ... ok\n\
|
|
test \"explicit local result ok_or_none string ok\" ... ok\n\
|
|
test \"explicit local result ok_or_none string none\" ... ok\n\
|
|
test \"explicit local result unwrap_or f64\" ... ok\n\
|
|
test \"explicit local result ok_or_none f64 ok\" ... ok\n\
|
|
test \"explicit local result ok_or_none f64 none\" ... ok\n\
|
|
test \"explicit local result bool helpers\" ... ok\n\
|
|
test \"explicit local result ok_or_none bool ok\" ... ok\n\
|
|
test \"explicit local result ok_or_none bool none\" ... ok\n\
|
|
test \"explicit local result helpers all\" ... ok\n\
|
|
26 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_runs_tests_in_topological_module_order() {
|
|
let project = write_project(
|
|
"test-order",
|
|
&[("a", "(module a (export value))\n\n(fn value () -> i32\n 1)\n\n(test \"provider first\"\n (= (value) 1))\n")],
|
|
"(module main)\n\n(import a (value))\n\n(fn main () -> i32\n (value))\n\n(test \"consumer second\"\n (= (value) 1))\n",
|
|
);
|
|
|
|
let output = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
|
|
assert_success_stdout(
|
|
"project test",
|
|
output,
|
|
"test \"provider first\" ... ok\ntest \"consumer second\" ... ok\n2 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_project_filter_preserves_order_and_counts_skips() {
|
|
let project = write_project(
|
|
"test-filter",
|
|
&[("a", "(module a (export value))\n\n(fn value () -> i32\n 1)\n\n(test \"provider first\"\n (= (value) 1))\n")],
|
|
"(module main)\n\n(import a (value))\n\n(fn main () -> i32\n (value))\n\n(test \"consumer second\"\n (= (value) 1))\n",
|
|
);
|
|
|
|
let output = run_glagol([
|
|
"test".as_ref(),
|
|
project.as_os_str(),
|
|
"--filter".as_ref(),
|
|
"consumer".as_ref(),
|
|
]);
|
|
|
|
assert_success_stdout(
|
|
"project filtered test",
|
|
output,
|
|
"test \"provider first\" ... skipped\n\
|
|
test \"consumer second\" ... ok\n\
|
|
1 test(s) passed (total_discovered 2, selected 1, passed 1, failed 0, skipped 1, filter \"consumer\")\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn manifest_records_project_fields_modules_and_import_edges() {
|
|
let project = write_project(
|
|
"manifest",
|
|
&[(
|
|
"math",
|
|
"(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n (+ value 1))\n",
|
|
)],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n (add_one 41))\n",
|
|
);
|
|
let manifest = unique_path("artifact-manifest");
|
|
|
|
let output = run_glagol([
|
|
"check".as_ref(),
|
|
"--manifest".as_ref(),
|
|
manifest.as_os_str(),
|
|
project.as_os_str(),
|
|
]);
|
|
|
|
assert_success("project manifest check", &output);
|
|
let artifact = fs::read_to_string(&manifest).expect("read artifact manifest");
|
|
assert_project_block_snapshot(
|
|
"success check project manifest",
|
|
&artifact,
|
|
&project,
|
|
r#" (project
|
|
(project_manifest "$PROJECT/slovo.toml")
|
|
(project_root "$PROJECT")
|
|
(source_root "src")
|
|
(project_name "manifest")
|
|
(entry_module "main")
|
|
(modules
|
|
(module
|
|
(name "math")
|
|
(path "$PROJECT/src/math.slo")
|
|
(imports)
|
|
)
|
|
(module
|
|
(name "main")
|
|
(path "$PROJECT/src/main.slo")
|
|
(imports
|
|
(import "math")
|
|
)
|
|
)
|
|
)
|
|
(import_edges
|
|
(import_edge
|
|
(from "main")
|
|
(to "math")
|
|
)
|
|
)
|
|
(diagnostics_count 0)
|
|
(diagnostic_artifacts)
|
|
(build_outputs)
|
|
)"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_local_packages_fixture_checks_tests_and_records_graph_manifest() {
|
|
let workspace =
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/workspaces/exp-5-local");
|
|
let manifest = unique_path("workspace-artifact-manifest");
|
|
|
|
let check = run_glagol([
|
|
"check".as_ref(),
|
|
"--manifest".as_ref(),
|
|
manifest.as_os_str(),
|
|
workspace.as_os_str(),
|
|
]);
|
|
|
|
assert_success("workspace check", &check);
|
|
let artifact = fs::read_to_string(&manifest).expect("read workspace artifact manifest");
|
|
assert_project_block_snapshot(
|
|
"workspace check manifest",
|
|
&artifact,
|
|
&workspace,
|
|
r#" (project
|
|
(project_manifest "$PROJECT/slovo.toml")
|
|
(project_root "$PROJECT")
|
|
(source_root "")
|
|
(project_name "exp-5-local")
|
|
(entry_module "main")
|
|
(workspace
|
|
(workspace_root "$PROJECT")
|
|
(workspace_manifest "$PROJECT/slovo.toml")
|
|
(members
|
|
(member "packages/app")
|
|
(member "packages/mathlib")
|
|
)
|
|
(packages
|
|
(package
|
|
(name "mathlib")
|
|
(version "0.1.0")
|
|
(root "$PROJECT/packages/mathlib")
|
|
(manifest "$PROJECT/packages/mathlib/slovo.toml")
|
|
(source_root "src")
|
|
(entry "main")
|
|
(test_count 1)
|
|
(modules
|
|
(module
|
|
(name "math")
|
|
(path "$PROJECT/packages/mathlib/src/math.slo")
|
|
(imports)
|
|
)
|
|
)
|
|
)
|
|
(package
|
|
(name "app")
|
|
(version "0.1.0")
|
|
(root "$PROJECT/packages/app")
|
|
(manifest "$PROJECT/packages/app/slovo.toml")
|
|
(source_root "src")
|
|
(entry "main")
|
|
(test_count 1)
|
|
(modules
|
|
(module
|
|
(name "main")
|
|
(path "$PROJECT/packages/app/src/main.slo")
|
|
(imports
|
|
(import "mathlib.math")
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
(package_dependency_edges
|
|
(package_dependency
|
|
(from "app")
|
|
(to "mathlib")
|
|
(kind "local-path")
|
|
(path "packages/mathlib")
|
|
)
|
|
)
|
|
(selected_build_entry_package)
|
|
)
|
|
(modules
|
|
(module
|
|
(name "mathlib.math")
|
|
(path "$PROJECT/packages/mathlib/src/math.slo")
|
|
(imports)
|
|
)
|
|
(module
|
|
(name "app.main")
|
|
(path "$PROJECT/packages/app/src/main.slo")
|
|
(imports
|
|
(import "mathlib.math")
|
|
)
|
|
)
|
|
)
|
|
(import_edges
|
|
(import_edge
|
|
(from "app.main")
|
|
(to "mathlib.math")
|
|
)
|
|
)
|
|
(diagnostics_count 0)
|
|
(diagnostic_artifacts)
|
|
(build_outputs)
|
|
)"#,
|
|
);
|
|
|
|
let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
|
|
assert_success_stdout(
|
|
"workspace test",
|
|
test,
|
|
"test \"mathlib add one\" ... ok\ntest \"package import add one\" ... ok\n2 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_imports_exported_enum_values_across_packages() {
|
|
let workspace = write_workspace(
|
|
"workspace-enum-imports",
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/readings\"]\n",
|
|
&[
|
|
WorkspacePackageSpec {
|
|
member: "packages/readings",
|
|
manifest: "[package]\nname = \"readings\"\nversion = \"0.1.0\"\n",
|
|
modules: &[(
|
|
"readings",
|
|
"(module readings (export Reading make score))\n\n\
|
|
(enum Reading\n Missing\n (Value i32))\n\n\
|
|
(fn make ((value i32)) -> Reading\n (Reading.Value value))\n\n\
|
|
(fn score ((reading Reading)) -> i32\n (match reading\n ((Reading.Missing)\n 0)\n ((Reading.Value payload)\n payload)))\n",
|
|
)],
|
|
},
|
|
WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nreadings = { path = \"../readings\" }\n",
|
|
modules: &[(
|
|
"main",
|
|
"(module main)\n\n\
|
|
(import readings.readings (Reading make score))\n\n\
|
|
(fn main () -> i32\n (let reading Reading (make 41))\n (score reading))\n\n\
|
|
(test \"workspace enum import\"\n (= (Reading.Value 41) (make 41)))\n",
|
|
)],
|
|
},
|
|
],
|
|
);
|
|
|
|
let check = run_glagol(["check".as_ref(), workspace.as_os_str()]);
|
|
assert_success_stdout("workspace enum import check", check, "");
|
|
|
|
let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
|
|
assert_success_stdout(
|
|
"workspace enum import test",
|
|
test,
|
|
"test \"workspace enum import\" ... ok\n1 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_imports_exported_direct_i64_enum_values_across_packages() {
|
|
let workspace = write_workspace(
|
|
"workspace-direct-i64-enum-imports",
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/readings\"]\n",
|
|
&[
|
|
WorkspacePackageSpec {
|
|
member: "packages/readings",
|
|
manifest: "[package]\nname = \"readings\"\nversion = \"0.1.0\"\n",
|
|
modules: &[(
|
|
"readings",
|
|
"(module readings (export WideReading make score))\n\n\
|
|
(enum WideReading\n Missing\n (Value i64))\n\n\
|
|
(fn make ((value i64)) -> WideReading\n (WideReading.Value value))\n\n\
|
|
(fn score ((reading WideReading)) -> i64\n (match reading\n ((WideReading.Missing)\n 0i64)\n ((WideReading.Value payload)\n payload)))\n",
|
|
)],
|
|
},
|
|
WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nreadings = { path = \"../readings\" }\n",
|
|
modules: &[(
|
|
"main",
|
|
"(module main)\n\n\
|
|
(import readings.readings (WideReading make score))\n\n\
|
|
(fn main () -> i32\n (if (= (score (make 4294967296i64)) 4294967296i64) 0 1))\n\n\
|
|
(test \"workspace i64 enum import\"\n (= (WideReading.Value 4294967296i64) (make 4294967296i64)))\n",
|
|
)],
|
|
},
|
|
],
|
|
);
|
|
|
|
let check = run_glagol(["check".as_ref(), workspace.as_os_str()]);
|
|
assert_success_stdout("workspace i64 enum import check", check, "");
|
|
|
|
let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
|
|
assert_success_stdout(
|
|
"workspace i64 enum import test",
|
|
test,
|
|
"test \"workspace i64 enum import\" ... ok\n1 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_build_smoke_uses_single_entry_package_when_host_toolchain_is_available() {
|
|
let workspace =
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/workspaces/exp-5-local");
|
|
let binary = unique_path("workspace-build-bin");
|
|
|
|
let output = run_glagol([
|
|
"build".as_ref(),
|
|
"-o".as_ref(),
|
|
binary.as_os_str(),
|
|
workspace.as_os_str(),
|
|
]);
|
|
|
|
if output.status.code() == Some(3) {
|
|
assert_stderr_contains("workspace build toolchain", &output, "ToolchainUnavailable");
|
|
return;
|
|
}
|
|
|
|
assert_success("workspace build", &output);
|
|
let run = Command::new(&binary)
|
|
.output()
|
|
.expect("run workspace build output");
|
|
assert_success("workspace build binary", &run);
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&run.stdout),
|
|
"42\n",
|
|
"workspace build binary stdout mismatch"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_build_requires_one_entry_package() {
|
|
let workspace = write_workspace(
|
|
"workspace-ambiguous-build-entry",
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/tool\"]\n",
|
|
&[
|
|
WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
|
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
|
},
|
|
WorkspacePackageSpec {
|
|
member: "packages/tool",
|
|
manifest: "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n",
|
|
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
|
},
|
|
],
|
|
);
|
|
let binary = unique_path("workspace-ambiguous-build-bin");
|
|
|
|
let output = run_glagol([
|
|
"build".as_ref(),
|
|
"-o".as_ref(),
|
|
binary.as_os_str(),
|
|
workspace.as_os_str(),
|
|
]);
|
|
|
|
assert_exit_code("workspace ambiguous entry", &output, 1);
|
|
assert_stderr_contains(
|
|
"workspace ambiguous entry",
|
|
&output,
|
|
"WorkspaceBuildAmbiguousEntryPackage",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_package_boundaries_are_diagnostics() {
|
|
let missing = write_workspace(
|
|
"workspace-missing-dependency",
|
|
"[workspace]\nmembers = [\"packages/app\"]\n",
|
|
&[WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmathlib = { path = \"../mathlib\" }\n",
|
|
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
|
}],
|
|
);
|
|
let missing_output = run_glagol(["check".as_ref(), missing.as_os_str()]);
|
|
assert_exit_code("missing package dependency", &missing_output, 1);
|
|
assert_stderr_contains(
|
|
"missing package dependency",
|
|
&missing_output,
|
|
"MissingPackageDependency",
|
|
);
|
|
|
|
let duplicate = write_workspace(
|
|
"workspace-duplicate-package",
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/dup\"]\n",
|
|
&[
|
|
WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
|
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
|
},
|
|
WorkspacePackageSpec {
|
|
member: "packages/dup",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
|
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
|
},
|
|
],
|
|
);
|
|
let duplicate_output = run_glagol(["check".as_ref(), duplicate.as_os_str()]);
|
|
assert_exit_code("duplicate package name", &duplicate_output, 1);
|
|
assert_stderr_contains(
|
|
"duplicate package name",
|
|
&duplicate_output,
|
|
"DuplicatePackageName",
|
|
);
|
|
|
|
let cycle = write_workspace(
|
|
"workspace-package-cycle",
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/mathlib\"]\n",
|
|
&[
|
|
WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmathlib = { path = \"../mathlib\" }\n",
|
|
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
|
},
|
|
WorkspacePackageSpec {
|
|
member: "packages/mathlib",
|
|
manifest: "[package]\nname = \"mathlib\"\nversion = \"0.1.0\"\n\n[dependencies]\napp = { path = \"../app\" }\n",
|
|
modules: &[("math", "(module math)\n\n(fn value () -> i32\n 1)\n")],
|
|
},
|
|
],
|
|
);
|
|
let cycle_output = run_glagol(["check".as_ref(), cycle.as_os_str()]);
|
|
assert_exit_code("package dependency cycle", &cycle_output, 1);
|
|
assert_stderr_contains(
|
|
"package dependency cycle",
|
|
&cycle_output,
|
|
"PackageDependencyCycle",
|
|
);
|
|
|
|
let private = write_workspace(
|
|
"workspace-private-visibility",
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/mathlib\"]\n",
|
|
&[
|
|
WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmathlib = { path = \"../mathlib\" }\n",
|
|
modules: &[(
|
|
"main",
|
|
"(module main)\n\n(import mathlib.math (add_one))\n\n(fn main () -> i32\n (add_one 1))\n",
|
|
)],
|
|
},
|
|
WorkspacePackageSpec {
|
|
member: "packages/mathlib",
|
|
manifest: "[package]\nname = \"mathlib\"\nversion = \"0.1.0\"\n",
|
|
modules: &[(
|
|
"math",
|
|
"(module math)\n\n(fn add_one ((value i32)) -> i32\n (+ value 1))\n",
|
|
)],
|
|
},
|
|
],
|
|
);
|
|
let private_output = run_glagol(["check".as_ref(), private.as_os_str()]);
|
|
assert_exit_code("private package visibility", &private_output, 1);
|
|
assert_stderr_contains("private package visibility", &private_output, "Visibility");
|
|
|
|
let invalid = write_workspace(
|
|
"workspace-invalid-package",
|
|
"[workspace]\nmembers = [\"packages/app\"]\n",
|
|
&[WorkspacePackageSpec {
|
|
member: "packages/app",
|
|
manifest: "[package]\nname = \"App\"\nversion = \"0.1\"\n",
|
|
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
|
}],
|
|
);
|
|
let invalid_output = run_glagol(["check".as_ref(), invalid.as_os_str()]);
|
|
assert_exit_code("invalid package metadata", &invalid_output, 1);
|
|
assert_stderr_contains(
|
|
"invalid package name",
|
|
&invalid_output,
|
|
"InvalidPackageName",
|
|
);
|
|
assert_stderr_contains(
|
|
"invalid package version",
|
|
&invalid_output,
|
|
"InvalidPackageVersion",
|
|
);
|
|
|
|
let escape = write_workspace(
|
|
"workspace-path-escape",
|
|
"[workspace]\nmembers = [\"../outside\"]\n",
|
|
&[],
|
|
);
|
|
let escape_output = run_glagol(["check".as_ref(), escape.as_os_str()]);
|
|
assert_exit_code("workspace member path escape", &escape_output, 1);
|
|
assert_stderr_contains(
|
|
"workspace member path escape",
|
|
&escape_output,
|
|
"WorkspaceMemberPathEscape",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn single_file_input_keeps_v1_2_behavior_even_next_to_manifest() {
|
|
let project = write_project(
|
|
"single-file",
|
|
&[(
|
|
"math",
|
|
"(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n (+ value 1))\n",
|
|
)],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n (add_one 41))\n",
|
|
);
|
|
let single = project.join("src/main.slo");
|
|
|
|
let output = run_glagol(["check".as_ref(), single.as_os_str()]);
|
|
|
|
assert_exit_code("single file project source", &output, 1);
|
|
assert_stderr_contains("single file remains v1.2", &output, "UnknownTopLevelForm");
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_run_tests_flag_does_not_enter_project_mode() {
|
|
let project = write_project(
|
|
"legacy-run-tests",
|
|
&[],
|
|
"(module main)\n\n(test \"ok\"\n true)\n",
|
|
);
|
|
|
|
let output = run_glagol(["--run-tests".as_ref(), project.as_os_str()]);
|
|
|
|
assert_exit_code("legacy --run-tests project", &output, 1);
|
|
assert_stderr_contains("legacy --run-tests project", &output, "InputReadFailed");
|
|
}
|
|
|
|
#[test]
|
|
fn project_check_and_test_do_not_require_entry_main_function() {
|
|
let project = write_project(
|
|
"check-test-no-main",
|
|
&[],
|
|
"(module main)\n\n(test \"ok\"\n true)\n",
|
|
);
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
|
|
assert_success_stdout("check without entry main", check, "");
|
|
assert_success_stdout(
|
|
"test without entry main",
|
|
test,
|
|
"test \"ok\" ... ok\n1 test(s) passed\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn diagnostic_uses_source_file_for_failed_project_test() {
|
|
let project = write_project(
|
|
"source-span",
|
|
&[],
|
|
"(module main)\n\n(fn main () -> i32\n 0)\n\n(test \"fails\"\n false)\n",
|
|
);
|
|
let output = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
let main_file = project.join("src/main.slo").display().to_string();
|
|
|
|
assert_exit_code("failed project test", &output, 1);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr.contains("TestFailed") && stderr.contains(&main_file),
|
|
"diagnostic did not reference source file `{}`:\n{}",
|
|
main_file,
|
|
stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn failure_manifest_records_project_block_and_diagnostics_artifact() {
|
|
let project = write_project(
|
|
"failure-manifest",
|
|
&[],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n (add_one 41))\n",
|
|
);
|
|
let manifest = unique_path("failure-artifact-manifest");
|
|
|
|
let output = run_glagol([
|
|
"check".as_ref(),
|
|
"--manifest".as_ref(),
|
|
manifest.as_os_str(),
|
|
project.as_os_str(),
|
|
]);
|
|
|
|
assert_exit_code("failure project manifest check", &output, 1);
|
|
let artifact = fs::read_to_string(&manifest).expect("read failure artifact manifest");
|
|
assert_project_block_snapshot(
|
|
"failure check project manifest",
|
|
&artifact,
|
|
&project,
|
|
r#" (project
|
|
(project_manifest "$PROJECT/slovo.toml")
|
|
(project_root "$PROJECT")
|
|
(source_root "src")
|
|
(project_name "failure-manifest")
|
|
(entry_module "main")
|
|
(modules
|
|
(module
|
|
(name "main")
|
|
(path "$PROJECT/src/main.slo")
|
|
(imports
|
|
(import "math")
|
|
)
|
|
)
|
|
)
|
|
(import_edges
|
|
(import_edge
|
|
(from "main")
|
|
(to "math")
|
|
)
|
|
)
|
|
(diagnostics_count 1)
|
|
(diagnostic_artifacts
|
|
(artifact
|
|
(kind diagnostics)
|
|
(stream stderr)
|
|
)
|
|
)
|
|
(build_outputs)
|
|
)"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_import_module_source_root_and_manifest_are_diagnostics() {
|
|
let project = write_project(
|
|
"missing-import",
|
|
&[],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n (add_one 41))\n",
|
|
);
|
|
let missing_import = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_exit_code("missing import", &missing_import, 1);
|
|
assert_stderr_contains("missing import", &missing_import, "MissingImport");
|
|
|
|
let missing_root = write_project_with_manifest(
|
|
"missing-root",
|
|
"[project]\nname = \"missing-root\"\nsource_root = \"missing\"\nentry = \"main\"\n",
|
|
&[],
|
|
"",
|
|
);
|
|
let missing_root_output = run_glagol(["check".as_ref(), missing_root.as_os_str()]);
|
|
assert_exit_code("missing source root", &missing_root_output, 1);
|
|
assert_stderr_contains(
|
|
"missing source root",
|
|
&missing_root_output,
|
|
"ProjectSourceRootMissing",
|
|
);
|
|
|
|
let bad_manifest = unique_path("bad-manifest");
|
|
fs::create_dir_all(&bad_manifest).expect("create bad manifest project");
|
|
fs::write(
|
|
bad_manifest.join("slovo.toml"),
|
|
"[project]\nentry = \"main\"\n",
|
|
)
|
|
.expect("write bad manifest");
|
|
let bad_manifest_output = run_glagol(["check".as_ref(), bad_manifest.as_os_str()]);
|
|
assert_exit_code("bad manifest", &bad_manifest_output, 1);
|
|
assert_stderr_contains(
|
|
"bad manifest",
|
|
&bad_manifest_output,
|
|
"ProjectManifestInvalid",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_manifest_boundaries_are_diagnostics() {
|
|
let cases = [
|
|
(
|
|
"missing-project-section",
|
|
"name = \"missing-section\"\n",
|
|
"project manifest is missing `[project]` section",
|
|
),
|
|
(
|
|
"invalid-name",
|
|
"[project]\nname = \"Bad_Name\"\n",
|
|
"project name must be lowercase ASCII",
|
|
),
|
|
(
|
|
"invalid-entry",
|
|
"[project]\nname = \"invalid-entry\"\nentry = \"../main\"\n",
|
|
"project entry must be a flat module identifier",
|
|
),
|
|
(
|
|
"source-root-parent",
|
|
"[project]\nname = \"source-root-parent\"\nsource_root = \"../src\"\n",
|
|
"project source_root must be a relative path",
|
|
),
|
|
(
|
|
"source-root-absolute",
|
|
"[project]\nname = \"source-root-absolute\"\nsource_root = \"/tmp\"\n",
|
|
"project source_root must be a relative path",
|
|
),
|
|
(
|
|
"generated-source",
|
|
"[project]\nname = \"generated-source\"\nsource_root = \"generated-source\"\n",
|
|
"project source_root must be a relative path",
|
|
),
|
|
];
|
|
|
|
for (name, manifest, expected) in cases {
|
|
let project = write_project_with_manifest(name, manifest, &[], "");
|
|
let output = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_exit_code(name, &output, 1);
|
|
assert_stderr_contains(name, &output, "ProjectManifestInvalid");
|
|
assert_stderr_contains(name, &output, expected);
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn symlink_source_root_and_module_file_escapes_are_diagnostics() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let root_escape = unique_path("symlink-root-escape");
|
|
let outside_root = unique_path("symlink-root-outside");
|
|
fs::create_dir_all(&root_escape).expect("create symlink project");
|
|
fs::create_dir_all(&outside_root).expect("create symlink outside root");
|
|
fs::write(
|
|
root_escape.join("slovo.toml"),
|
|
"[project]\nname = \"symlink-root-escape\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
)
|
|
.expect("write symlink manifest");
|
|
symlink(&outside_root, root_escape.join("src")).expect("symlink source root");
|
|
let root_output = run_glagol(["check".as_ref(), root_escape.as_os_str()]);
|
|
assert_exit_code("symlink source root escape", &root_output, 1);
|
|
assert_stderr_contains(
|
|
"symlink source root escape",
|
|
&root_output,
|
|
"ProjectManifestInvalid",
|
|
);
|
|
assert_stderr_contains(
|
|
"symlink source root escape",
|
|
&root_output,
|
|
"source_root must not escape",
|
|
);
|
|
|
|
let module_escape = unique_path("symlink-module-escape");
|
|
let outside_module = unique_path("symlink-module-outside");
|
|
fs::create_dir_all(module_escape.join("src")).expect("create module escape src");
|
|
fs::create_dir_all(&outside_module).expect("create module outside dir");
|
|
fs::write(
|
|
module_escape.join("slovo.toml"),
|
|
"[project]\nname = \"symlink-module-escape\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
)
|
|
.expect("write module escape manifest");
|
|
let outside_main = outside_module.join("main.slo");
|
|
fs::write(&outside_main, "(module main)\n\n(fn main () -> i32\n 0)\n")
|
|
.expect("write outside module");
|
|
symlink(outside_main, module_escape.join("src/main.slo")).expect("symlink module file");
|
|
let module_output = run_glagol(["check".as_ref(), module_escape.as_os_str()]);
|
|
assert_exit_code("symlink module escape", &module_output, 1);
|
|
assert_stderr_contains(
|
|
"symlink module escape",
|
|
&module_output,
|
|
"ProjectManifestInvalid",
|
|
);
|
|
assert_stderr_contains(
|
|
"symlink module escape",
|
|
&module_output,
|
|
"escapes the source root",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_visibility_ambiguous_and_import_cycle_are_diagnostics() {
|
|
let duplicate = write_project(
|
|
"duplicate",
|
|
&[("math", "(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n (+ value 1))\n")],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn add_one ((value i32)) -> i32\n value)\n\n(fn main () -> i32\n (add_one 41))\n",
|
|
);
|
|
let duplicate_output = run_glagol(["check".as_ref(), duplicate.as_os_str()]);
|
|
assert_exit_code("duplicate", &duplicate_output, 1);
|
|
assert_stderr_contains("duplicate", &duplicate_output, "DuplicateName");
|
|
|
|
let visibility = write_project(
|
|
"visibility",
|
|
&[("math", "(module math)\n\n(fn hidden () -> i32\n 1)\n")],
|
|
"(module main)\n\n(import math (hidden))\n\n(fn main () -> i32\n (hidden))\n",
|
|
);
|
|
let visibility_output = run_glagol(["check".as_ref(), visibility.as_os_str()]);
|
|
assert_exit_code("visibility", &visibility_output, 1);
|
|
assert_stderr_contains("visibility", &visibility_output, "Visibility");
|
|
|
|
let ambiguous = write_project(
|
|
"ambiguous",
|
|
&[
|
|
("left", "(module left (export value))\n\n(fn value () -> i32\n 1)\n"),
|
|
("right", "(module right (export value))\n\n(fn value () -> i32\n 2)\n"),
|
|
],
|
|
"(module main)\n\n(import left (value))\n(import right (value))\n\n(fn main () -> i32\n (value))\n",
|
|
);
|
|
let ambiguous_output = run_glagol(["check".as_ref(), ambiguous.as_os_str()]);
|
|
assert_exit_code("ambiguous", &ambiguous_output, 1);
|
|
assert_stderr_contains("ambiguous", &ambiguous_output, "AmbiguousName");
|
|
|
|
let cycle = write_project(
|
|
"cycle",
|
|
&[
|
|
(
|
|
"a",
|
|
"(module a (export fa))\n\n(import b (fb))\n\n(fn fa () -> i32\n (fb))\n",
|
|
),
|
|
(
|
|
"b",
|
|
"(module b (export fb))\n\n(import a (fa))\n\n(fn fb () -> i32\n (fa))\n",
|
|
),
|
|
],
|
|
"(module main)\n\n(import a (fa))\n\n(fn main () -> i32\n (fa))\n",
|
|
);
|
|
let cycle_output = run_glagol(["check".as_ref(), cycle.as_os_str()]);
|
|
assert_exit_code("cycle", &cycle_output, 1);
|
|
assert_stderr_contains("cycle", &cycle_output, "ImportCycle");
|
|
assert_stderr_contains(
|
|
"cycle related edge",
|
|
&cycle_output,
|
|
"module `a` imports `b`",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn enum_import_visibility_and_duplicate_cases_are_diagnostics() {
|
|
let private = write_project(
|
|
"enum-private-import",
|
|
&[(
|
|
"readings",
|
|
"(module readings)\n\n(enum Reading Missing (Value i32))\n",
|
|
)],
|
|
"(module main)\n\n(import readings (Reading))\n",
|
|
);
|
|
let private_output = run_glagol(["check".as_ref(), private.as_os_str()]);
|
|
assert_exit_code("private enum import", &private_output, 1);
|
|
assert_stderr_contains("private enum import", &private_output, "Visibility");
|
|
assert_stderr_contains(
|
|
"private enum related span",
|
|
&private_output,
|
|
"private declaration",
|
|
);
|
|
|
|
let missing = write_project(
|
|
"enum-missing-import",
|
|
&[(
|
|
"readings",
|
|
"(module readings)\n\n(fn main () -> i32\n 0)\n",
|
|
)],
|
|
"(module main)\n\n(import readings (Reading))\n",
|
|
);
|
|
let missing_output = run_glagol(["check".as_ref(), missing.as_os_str()]);
|
|
assert_exit_code("missing enum import", &missing_output, 1);
|
|
assert_stderr_contains("missing enum import", &missing_output, "MissingImport");
|
|
assert_stderr_contains(
|
|
"missing enum import message",
|
|
&missing_output,
|
|
"function, struct, or enum",
|
|
);
|
|
|
|
let duplicate = write_project(
|
|
"enum-duplicate-import",
|
|
&[(
|
|
"readings",
|
|
"(module readings (export Reading))\n\n(enum Reading Missing)\n",
|
|
)],
|
|
"(module main)\n\n(import readings (Reading))\n\n(enum Reading Local)\n",
|
|
);
|
|
let duplicate_output = run_glagol(["check".as_ref(), duplicate.as_os_str()]);
|
|
assert_exit_code("duplicate enum import", &duplicate_output, 1);
|
|
assert_stderr_contains("duplicate enum import", &duplicate_output, "DuplicateName");
|
|
|
|
let invalid_export = write_project(
|
|
"enum-invalid-export",
|
|
&[("readings", "(module readings (export Reading))\n")],
|
|
"(module main)\n",
|
|
);
|
|
let invalid_export_output = run_glagol(["check".as_ref(), invalid_export.as_os_str()]);
|
|
assert_exit_code("invalid enum export", &invalid_export_output, 1);
|
|
assert_stderr_contains("invalid enum export", &invalid_export_output, "Visibility");
|
|
assert_stderr_contains(
|
|
"invalid enum export message",
|
|
&invalid_export_output,
|
|
"local function, struct, or enum",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_diagnostic_families_have_json_golden_coverage() {
|
|
let duplicate = write_project(
|
|
"json-duplicate",
|
|
&[(
|
|
"math",
|
|
"(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n (+ value 1))\n",
|
|
)],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn add_one ((value i32)) -> i32\n value)\n",
|
|
);
|
|
assert_json_diagnostic_code("duplicate json", &duplicate, "DuplicateName");
|
|
|
|
let missing = write_project(
|
|
"json-missing",
|
|
&[],
|
|
"(module main)\n\n(import math (add_one))\n",
|
|
);
|
|
assert_json_diagnostic_code("missing json", &missing, "MissingImport");
|
|
|
|
let visibility = write_project(
|
|
"json-visibility",
|
|
&[("math", "(module math)\n\n(fn hidden () -> i32\n 1)\n")],
|
|
"(module main)\n\n(import math (hidden))\n",
|
|
);
|
|
assert_json_diagnostic_code("visibility json", &visibility, "Visibility");
|
|
|
|
let ambiguous = write_project(
|
|
"json-ambiguous",
|
|
&[
|
|
(
|
|
"left",
|
|
"(module left (export value))\n\n(fn value () -> i32\n 1)\n",
|
|
),
|
|
(
|
|
"right",
|
|
"(module right (export value))\n\n(fn value () -> i32\n 2)\n",
|
|
),
|
|
],
|
|
"(module main)\n\n(import left (value))\n(import right (value))\n",
|
|
);
|
|
assert_json_diagnostic_code("ambiguous json", &ambiguous, "AmbiguousName");
|
|
|
|
let cycle = write_project(
|
|
"json-cycle",
|
|
&[
|
|
(
|
|
"a",
|
|
"(module a (export fa))\n\n(import b (fb))\n\n(fn fa () -> i32\n (fb))\n",
|
|
),
|
|
(
|
|
"b",
|
|
"(module b (export fb))\n\n(import a (fa))\n\n(fn fb () -> i32\n (fa))\n",
|
|
),
|
|
],
|
|
"(module main)\n\n(import a (fa))\n",
|
|
);
|
|
assert_json_diagnostic_code("cycle json", &cycle, "ImportCycle");
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_related_spans_render_file_identity_in_text_sexpr_and_json() {
|
|
let visibility = write_project(
|
|
"related-visibility",
|
|
&[("math", "(module math)\n\n(fn hidden () -> i32\n 1)\n")],
|
|
"(module main)\n\n(import math (hidden))\n",
|
|
);
|
|
let text = run_glagol(["check".as_ref(), visibility.as_os_str()]);
|
|
assert_exit_code("visibility related text", &text, 1);
|
|
let stderr = String::from_utf8_lossy(&text.stderr);
|
|
let math_file = visibility.join("src/math.slo").display().to_string();
|
|
let main_file = visibility.join("src/main.slo").display().to_string();
|
|
assert!(
|
|
stderr.contains(&format!("(file {})", sexpr_string(&main_file)))
|
|
&& stderr.contains(&format!("(file {})", sexpr_string(&math_file)))
|
|
&& stderr.contains("private declaration"),
|
|
"visibility diagnostic did not carry cross-file related span:\n{}",
|
|
stderr
|
|
);
|
|
|
|
let json = run_glagol([
|
|
"check".as_ref(),
|
|
"--json-diagnostics".as_ref(),
|
|
visibility.as_os_str(),
|
|
]);
|
|
assert_exit_code("visibility related json", &json, 1);
|
|
let stderr = String::from_utf8_lossy(&json.stderr);
|
|
assert!(
|
|
stderr.contains(&format!("\"file\":{}", json_string(&main_file)))
|
|
&& stderr.contains(&format!("\"file\":{}", json_string(&math_file)))
|
|
&& stderr.contains("\"related\"")
|
|
&& stderr.contains("private declaration"),
|
|
"visibility JSON diagnostic did not carry cross-file related span:\n{}",
|
|
stderr
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_build_smoke_uses_entry_main_when_host_toolchain_is_available() {
|
|
let project = write_project(
|
|
"build",
|
|
&[("math", "(module math (export main add_one))\n\n(fn main () -> i32\n 9)\n\n(fn add_one ((value i32)) -> i32\n (+ value 1))\n")],
|
|
"(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n (print_i32 (add_one 41))\n 0)\n",
|
|
);
|
|
let binary = unique_path("project-build-bin");
|
|
|
|
let output = run_glagol([
|
|
"build".as_ref(),
|
|
"-o".as_ref(),
|
|
binary.as_os_str(),
|
|
project.as_os_str(),
|
|
]);
|
|
|
|
if output.status.code() == Some(3) {
|
|
assert_stderr_contains("project build toolchain", &output, "ToolchainUnavailable");
|
|
return;
|
|
}
|
|
|
|
assert_success("project build", &output);
|
|
let run = Command::new(&binary)
|
|
.output()
|
|
.expect("run project build output");
|
|
assert_success("project build binary", &run);
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&run.stdout),
|
|
"42\n",
|
|
"project build binary stdout mismatch"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_build_requires_entry_main_contract() {
|
|
let missing_main = write_project(
|
|
"missing-main",
|
|
&[],
|
|
"(module main)\n\n(test \"ok\"\n true)\n",
|
|
);
|
|
let missing_binary = unique_path("missing-main-bin");
|
|
let missing_output = run_glagol([
|
|
"build".as_ref(),
|
|
"-o".as_ref(),
|
|
missing_binary.as_os_str(),
|
|
missing_main.as_os_str(),
|
|
]);
|
|
assert_exit_code("missing entry main", &missing_output, 1);
|
|
assert_stderr_contains("missing entry main", &missing_output, "MissingImport");
|
|
|
|
let bad_signature = write_project(
|
|
"bad-main-signature",
|
|
&[],
|
|
"(module main)\n\n(fn main ((value i32)) -> i32\n value)\n",
|
|
);
|
|
let bad_binary = unique_path("bad-main-signature-bin");
|
|
let bad_output = run_glagol([
|
|
"build".as_ref(),
|
|
"-o".as_ref(),
|
|
bad_binary.as_os_str(),
|
|
bad_signature.as_os_str(),
|
|
]);
|
|
assert_exit_code("bad entry main signature", &bad_output, 1);
|
|
assert_stderr_contains("bad entry main signature", &bad_output, "MissingImport");
|
|
}
|
|
|
|
#[test]
|
|
fn module_mismatch_and_reserved_intrinsics_are_diagnostics() {
|
|
let mismatch = write_project_with_manifest(
|
|
"module-mismatch",
|
|
"[project]\nname = \"module-mismatch\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
&[("main", "(module wrong)\n\n(fn main () -> i32\n 0)\n")],
|
|
"",
|
|
);
|
|
let mismatch_output = run_glagol(["check".as_ref(), mismatch.as_os_str()]);
|
|
assert_exit_code("module mismatch", &mismatch_output, 1);
|
|
assert_stderr_contains("module mismatch", &mismatch_output, "MissingImport");
|
|
|
|
let local_reserved = write_project(
|
|
"local-reserved",
|
|
&[],
|
|
"(module main)\n\n(fn print_i32 () -> i32\n 0)\n",
|
|
);
|
|
let local_output = run_glagol(["check".as_ref(), local_reserved.as_os_str()]);
|
|
assert_exit_code("local reserved", &local_output, 1);
|
|
assert_stderr_contains("local reserved", &local_output, "DuplicateName");
|
|
|
|
let export_reserved = write_project(
|
|
"export-reserved",
|
|
&[],
|
|
"(module main (export string_len))\n\n(fn main () -> i32\n 0)\n",
|
|
);
|
|
let export_output = run_glagol(["check".as_ref(), export_reserved.as_os_str()]);
|
|
assert_exit_code("export reserved", &export_output, 1);
|
|
assert_stderr_contains("export reserved", &export_output, "Visibility");
|
|
|
|
let import_reserved = write_project(
|
|
"import-reserved",
|
|
&[("math", "(module math)\n\n(fn main () -> i32\n 0)\n")],
|
|
"(module main)\n\n(import math (print_i32))\n",
|
|
);
|
|
let import_output = run_glagol(["check".as_ref(), import_reserved.as_os_str()]);
|
|
assert_exit_code("import reserved", &import_output, 1);
|
|
assert_stderr_contains("import reserved", &import_output, "Visibility");
|
|
|
|
let local_promoted_reserved = write_project(
|
|
"local-promoted-reserved",
|
|
&[],
|
|
"(module main)\n\n(fn std.io.print_i32 () -> i32\n 0)\n",
|
|
);
|
|
let local_promoted_output = run_glagol(["check".as_ref(), local_promoted_reserved.as_os_str()]);
|
|
assert_exit_code("local promoted reserved", &local_promoted_output, 1);
|
|
assert_stderr_contains(
|
|
"local promoted reserved",
|
|
&local_promoted_output,
|
|
"DuplicateName",
|
|
);
|
|
|
|
let local_private_runtime_reserved = write_project(
|
|
"local-private-runtime-reserved",
|
|
&[],
|
|
"(module main)\n\n(fn __glagol_string_concat ((left string) (right string)) -> string\n \"intercepted\")\n",
|
|
);
|
|
let local_private_runtime_output =
|
|
run_glagol(["check".as_ref(), local_private_runtime_reserved.as_os_str()]);
|
|
assert_exit_code(
|
|
"local private runtime reserved",
|
|
&local_private_runtime_output,
|
|
1,
|
|
);
|
|
assert_stderr_contains(
|
|
"local private runtime reserved",
|
|
&local_private_runtime_output,
|
|
"DuplicateName",
|
|
);
|
|
|
|
let struct_promoted_reserved = write_project(
|
|
"struct-promoted-reserved",
|
|
&[],
|
|
"(module main)\n\n(struct std.string.len (value i32))\n\n(fn main () -> i32\n 0)\n",
|
|
);
|
|
let struct_promoted_output =
|
|
run_glagol(["check".as_ref(), struct_promoted_reserved.as_os_str()]);
|
|
assert_exit_code("struct promoted reserved", &struct_promoted_output, 1);
|
|
assert_stderr_contains(
|
|
"struct promoted reserved",
|
|
&struct_promoted_output,
|
|
"DuplicateName",
|
|
);
|
|
|
|
let export_promoted_reserved = write_project(
|
|
"export-promoted-reserved",
|
|
&[],
|
|
"(module main (export std.string.len))\n\n(fn main () -> i32\n 0)\n",
|
|
);
|
|
let export_promoted_output =
|
|
run_glagol(["check".as_ref(), export_promoted_reserved.as_os_str()]);
|
|
assert_exit_code("export promoted reserved", &export_promoted_output, 1);
|
|
assert_stderr_contains(
|
|
"export promoted reserved",
|
|
&export_promoted_output,
|
|
"Visibility",
|
|
);
|
|
|
|
let import_promoted_reserved = write_project(
|
|
"import-promoted-reserved",
|
|
&[("math", "(module math)\n\n(fn main () -> i32\n 0)\n")],
|
|
"(module main)\n\n(import math (std.io.print_i32))\n",
|
|
);
|
|
let import_promoted_output =
|
|
run_glagol(["check".as_ref(), import_promoted_reserved.as_os_str()]);
|
|
assert_exit_code("import promoted reserved", &import_promoted_output, 1);
|
|
assert_stderr_contains(
|
|
"import promoted reserved",
|
|
&import_promoted_output,
|
|
"Visibility",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unsafe_operation_heads_are_reserved_in_project_visibility() {
|
|
for head in UNSAFE_HEADS {
|
|
let project_head = head.replace('_', "-");
|
|
let local_reserved = write_project(
|
|
&format!("unsafe-local-reserved-{}", project_head),
|
|
&[],
|
|
&format!("(module main)\n\n(fn {} () -> i32\n 0)\n", head),
|
|
);
|
|
let local_output = run_glagol(["check".as_ref(), local_reserved.as_os_str()]);
|
|
assert_exit_code("unsafe local reserved", &local_output, 1);
|
|
assert_stderr_contains("unsafe local reserved", &local_output, "DuplicateName");
|
|
|
|
let export_reserved = write_project(
|
|
&format!("unsafe-export-reserved-{}", project_head),
|
|
&[],
|
|
&format!(
|
|
"(module main (export {}))\n\n(fn main () -> i32\n 0)\n",
|
|
head
|
|
),
|
|
);
|
|
let export_output = run_glagol(["check".as_ref(), export_reserved.as_os_str()]);
|
|
assert_exit_code("unsafe export reserved", &export_output, 1);
|
|
assert_stderr_contains("unsafe export reserved", &export_output, "Visibility");
|
|
|
|
let import_reserved = write_project(
|
|
&format!("unsafe-import-reserved-{}", project_head),
|
|
&[("math", "(module math)\n\n(fn main () -> i32\n 0)\n")],
|
|
&format!("(module main)\n\n(import math ({}))\n", head),
|
|
);
|
|
let import_output = run_glagol(["check".as_ref(), import_reserved.as_os_str()]);
|
|
assert_exit_code("unsafe import reserved", &import_output, 1);
|
|
assert_stderr_contains("unsafe import reserved", &import_output, "Visibility");
|
|
}
|
|
}
|
|
|
|
fn write_project(name: &str, modules: &[(&str, &str)], main: &str) -> PathBuf {
|
|
write_project_with_manifest(
|
|
name,
|
|
&format!(
|
|
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
name
|
|
),
|
|
modules,
|
|
main,
|
|
)
|
|
}
|
|
|
|
fn write_project_with_manifest(
|
|
name: &str,
|
|
manifest: &str,
|
|
modules: &[(&str, &str)],
|
|
main: &str,
|
|
) -> PathBuf {
|
|
let root = unique_path(name);
|
|
let src = root.join("src");
|
|
fs::create_dir_all(&src).expect("create project src");
|
|
fs::write(root.join("slovo.toml"), manifest).expect("write manifest");
|
|
if !main.is_empty() {
|
|
fs::write(src.join("main.slo"), main).expect("write main module");
|
|
}
|
|
for (module, source) in modules {
|
|
fs::write(src.join(format!("{}.slo", module)), source).expect("write module");
|
|
}
|
|
root
|
|
}
|
|
|
|
struct WorkspacePackageSpec<'a> {
|
|
member: &'a str,
|
|
manifest: &'a str,
|
|
modules: &'a [(&'a str, &'a str)],
|
|
}
|
|
|
|
fn write_workspace(
|
|
name: &str,
|
|
workspace_manifest: &str,
|
|
packages: &[WorkspacePackageSpec<'_>],
|
|
) -> PathBuf {
|
|
let root = unique_path(name);
|
|
fs::create_dir_all(&root).expect("create workspace root");
|
|
fs::write(root.join("slovo.toml"), workspace_manifest).expect("write workspace manifest");
|
|
for package in packages {
|
|
let package_root = root.join(package.member);
|
|
let src = package_root.join("src");
|
|
fs::create_dir_all(&src).expect("create package src");
|
|
fs::write(package_root.join("slovo.toml"), package.manifest)
|
|
.expect("write package manifest");
|
|
for (module, source) in package.modules {
|
|
fs::write(src.join(format!("{}.slo", module)), source).expect("write package module");
|
|
}
|
|
}
|
|
root
|
|
}
|
|
|
|
fn unique_path(name: &str) -> PathBuf {
|
|
let id = NEXT_PROJECT_ID.fetch_add(1, Ordering::SeqCst);
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|duration| duration.as_nanos())
|
|
.unwrap_or(0);
|
|
std::env::temp_dir().join(format!(
|
|
"glagol-project-mode-{}-{}-{}-{}",
|
|
std::process::id(),
|
|
nanos,
|
|
id,
|
|
name
|
|
))
|
|
}
|
|
|
|
fn run_glagol<I, S>(args: I) -> Output
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<std::ffi::OsStr>,
|
|
{
|
|
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
|
.args(args)
|
|
.output()
|
|
.expect("run glagol")
|
|
}
|
|
|
|
fn assert_success(context: &str, output: &Output) {
|
|
assert!(
|
|
output.status.success(),
|
|
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
|
context,
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn assert_success_stdout(context: &str, output: Output, expected: &str) {
|
|
assert_success(context, &output);
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&output.stdout),
|
|
expected,
|
|
"{} stdout mismatch",
|
|
context
|
|
);
|
|
assert!(
|
|
output.stderr.is_empty(),
|
|
"{} wrote stderr:\n{}",
|
|
context,
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn assert_exit_code(context: &str, output: &Output, expected: i32) {
|
|
assert_eq!(
|
|
output.status.code(),
|
|
Some(expected),
|
|
"{} exit code mismatch\nstdout:\n{}\nstderr:\n{}",
|
|
context,
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn assert_stderr_contains(context: &str, output: &Output, needle: &str) {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr.contains(needle),
|
|
"{} stderr did not contain `{}`:\n{}",
|
|
context,
|
|
needle,
|
|
stderr
|
|
);
|
|
}
|
|
|
|
fn assert_json_diagnostic_code(context: &str, project: &Path, code: &str) {
|
|
let output = run_glagol([
|
|
"check".as_ref(),
|
|
"--json-diagnostics".as_ref(),
|
|
project.as_os_str(),
|
|
]);
|
|
assert_exit_code(context, &output, 1);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr
|
|
.lines()
|
|
.any(|line| line.contains(&format!("\"code\":{}", json_string(code)))),
|
|
"{} JSON diagnostic did not contain code `{}`:\n{}",
|
|
context,
|
|
code,
|
|
stderr
|
|
);
|
|
}
|
|
|
|
fn assert_project_block_snapshot(context: &str, manifest: &str, project: &Path, expected: &str) {
|
|
let block = project_block(manifest);
|
|
let normalized = block.replace(&project.display().to_string(), "$PROJECT");
|
|
assert_eq!(
|
|
normalized, expected,
|
|
"{} project manifest block mismatch\nfull manifest:\n{}",
|
|
context, manifest
|
|
);
|
|
}
|
|
|
|
fn project_block(manifest: &str) -> &str {
|
|
let start = manifest
|
|
.find(" (project\n")
|
|
.expect("manifest did not contain project block");
|
|
manifest[start..]
|
|
.strip_suffix("\n)\n")
|
|
.expect("manifest did not end with artifact-manifest close")
|
|
}
|
|
|
|
fn sexpr_string(value: &str) -> String {
|
|
let mut out = String::from("\"");
|
|
for ch in value.chars() {
|
|
match ch {
|
|
'\\' => out.push_str("\\\\"),
|
|
'"' => out.push_str("\\\""),
|
|
'\n' => out.push_str("\\n"),
|
|
'\r' => out.push_str("\\r"),
|
|
'\t' => out.push_str("\\t"),
|
|
ch => out.push(ch),
|
|
}
|
|
}
|
|
out.push('"');
|
|
out
|
|
}
|
|
|
|
fn json_string(value: &str) -> String {
|
|
let mut out = String::from("\"");
|
|
for ch in value.chars() {
|
|
match ch {
|
|
'\\' => out.push_str("\\\\"),
|
|
'"' => out.push_str("\\\""),
|
|
'\n' => out.push_str("\\n"),
|
|
'\r' => out.push_str("\\r"),
|
|
'\t' => out.push_str("\\t"),
|
|
'\u{08}' => out.push_str("\\b"),
|
|
'\u{0c}' => out.push_str("\\f"),
|
|
ch if ch <= '\u{1f}' => out.push_str(&format!("\\u{:04x}", ch as u32)),
|
|
ch => out.push(ch),
|
|
}
|
|
}
|
|
out.push('"');
|
|
out
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn _assert_path_exists(path: &Path) {
|
|
assert!(path.exists(), "path did not exist: {}", path.display());
|
|
}
|