548 lines
19 KiB
Rust
548 lines
19 KiB
Rust
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process::{Command, Output},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
};
|
|
|
|
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
#[test]
|
|
fn new_creates_minimal_valid_project() {
|
|
let project = unique_path("new-project");
|
|
|
|
let output = run_glagol(["new".as_ref(), project.as_os_str()]);
|
|
|
|
assert_success("glagol new", &output);
|
|
assert!(output.stdout.is_empty(), "new wrote stdout");
|
|
assert!(output.stderr.is_empty(), "new wrote stderr");
|
|
assert_eq!(
|
|
fs::read_to_string(project.join("slovo.toml")).expect("read manifest"),
|
|
format!(
|
|
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
project.file_name().unwrap().to_string_lossy()
|
|
)
|
|
);
|
|
assert_eq!(
|
|
fs::read_to_string(project.join("src/main.slo")).expect("read main"),
|
|
"(module main)\n\n(fn main () -> i32\n 0)\n\n(test \"main returns zero\"\n (= (main) 0))\n"
|
|
);
|
|
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success("generated project check", &check);
|
|
let fmt_check = run_glagol(["fmt".as_ref(), "--check".as_ref(), project.as_os_str()]);
|
|
assert_success("generated project fmt check", &fmt_check);
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
assert_success("generated project test", &test);
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&test.stdout),
|
|
"test \"main returns zero\" ... ok\n1 test(s) passed\n"
|
|
);
|
|
let docs = unique_path("new-project-docs");
|
|
let doc = run_glagol([
|
|
"doc".as_ref(),
|
|
project.as_os_str(),
|
|
"-o".as_ref(),
|
|
docs.as_os_str(),
|
|
]);
|
|
assert_success("generated project docs", &doc);
|
|
let doc_index = fs::read_to_string(docs.join("index.md")).expect("read generated docs");
|
|
assert!(doc_index.contains("main returns zero"));
|
|
|
|
let binary = unique_path("new-project-bin");
|
|
let build = run_glagol([
|
|
"build".as_ref(),
|
|
project.as_os_str(),
|
|
"-o".as_ref(),
|
|
binary.as_os_str(),
|
|
]);
|
|
if build.status.success() {
|
|
let run = Command::new(&binary)
|
|
.output()
|
|
.expect("run generated project");
|
|
assert_success("generated project binary", &run);
|
|
assert!(
|
|
run.stdout.is_empty(),
|
|
"generated project binary wrote stdout"
|
|
);
|
|
} else {
|
|
assert_stderr_contains("generated project build", &build, "ToolchainUnavailable");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn run_builds_executes_and_clean_removes_generated_artifacts() {
|
|
let project = unique_path("run-project");
|
|
|
|
let new_output = run_glagol(["new".as_ref(), project.as_os_str()]);
|
|
assert_success("glagol new for run", &new_output);
|
|
|
|
let run = run_glagol(["run".as_ref(), project.as_os_str()]);
|
|
if run.status.success() {
|
|
assert!(run.stdout.is_empty(), "run wrote stdout");
|
|
assert!(run.stderr.is_empty(), "run wrote stderr");
|
|
assert!(
|
|
project.join(".slovo/build").is_dir(),
|
|
"run did not create generated build directory"
|
|
);
|
|
} else {
|
|
assert_stderr_contains("generated project run", &run, "ToolchainUnavailable");
|
|
fs::create_dir_all(project.join(".slovo/build")).expect("create synthetic build dir");
|
|
fs::write(project.join(".slovo/build/stale"), "").expect("write stale build file");
|
|
}
|
|
|
|
let clean = run_glagol(["clean".as_ref(), project.as_os_str()]);
|
|
assert_success("glagol clean", &clean);
|
|
assert!(clean.stdout.is_empty(), "clean wrote stdout");
|
|
assert!(clean.stderr.is_empty(), "clean wrote stderr");
|
|
assert!(
|
|
!project.join(".slovo/build").exists(),
|
|
"clean left generated build directory behind"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn run_forwards_program_arguments_when_host_toolchain_is_available() {
|
|
let project = write_project(
|
|
"run-args-project",
|
|
&[],
|
|
"(module main)\n\n(import std.process (argc))\n\n(fn main () -> i32\n (if (= (argc) 3)\n 0\n 1))\n",
|
|
);
|
|
|
|
let run = run_glagol([
|
|
"run".as_ref(),
|
|
project.as_os_str(),
|
|
"--".as_ref(),
|
|
"alpha".as_ref(),
|
|
"beta".as_ref(),
|
|
]);
|
|
|
|
if run.status.success() {
|
|
assert!(run.stdout.is_empty(), "run args wrote stdout");
|
|
assert!(run.stderr.is_empty(), "run args wrote stderr");
|
|
} else {
|
|
assert_stderr_contains("run args project", &run, "ToolchainUnavailable");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn installed_layout_discovers_std_and_runtime_without_checkout_paths() {
|
|
let prefix = unique_path("installed-layout");
|
|
let bin_dir = prefix.join("bin");
|
|
let std_dir = prefix.join("share/slovo/std");
|
|
let runtime_dir = prefix.join("share/slovo/runtime");
|
|
fs::create_dir_all(&bin_dir).expect("create installed bin dir");
|
|
fs::create_dir_all(&std_dir).expect("create installed std dir");
|
|
fs::create_dir_all(&runtime_dir).expect("create installed runtime dir");
|
|
|
|
let installed_glagol = bin_dir.join(format!("glagol{}", std::env::consts::EXE_SUFFIX));
|
|
fs::copy(env!("CARGO_BIN_EXE_glagol"), &installed_glagol).expect("copy glagol");
|
|
make_executable(&installed_glagol);
|
|
|
|
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.expect("compiler has repo parent");
|
|
fs::copy(
|
|
repo_root.join("runtime/runtime.c"),
|
|
runtime_dir.join("runtime.c"),
|
|
)
|
|
.expect("copy runtime");
|
|
for entry in fs::read_dir(repo_root.join("lib/std")).expect("read std dir") {
|
|
let entry = entry.expect("read std entry");
|
|
let path = entry.path();
|
|
if path.extension().and_then(|ext| ext.to_str()) == Some("slo") {
|
|
fs::copy(
|
|
&path,
|
|
std_dir.join(path.file_name().expect("std file name")),
|
|
)
|
|
.expect("copy std module");
|
|
}
|
|
}
|
|
|
|
let project = write_project(
|
|
"installed-layout-project",
|
|
&[],
|
|
"(module main)\n\n(import std.io (print_string_zero))\n(import std.string (concat))\n\n(fn main () -> i32\n (print_string_zero (concat \"installed\" \"-ok\")))\n",
|
|
);
|
|
|
|
let check = run_installed_glagol(&installed_glagol, ["check".as_ref(), project.as_os_str()]);
|
|
assert_success("installed layout check", &check);
|
|
|
|
let run = run_installed_glagol(&installed_glagol, ["run".as_ref(), project.as_os_str()]);
|
|
if host_clang_available() {
|
|
assert_success("installed layout run", &run);
|
|
assert_eq!(String::from_utf8_lossy(&run.stdout), "installed-ok\n");
|
|
assert!(run.stderr.is_empty(), "installed run wrote stderr");
|
|
} else {
|
|
assert_exit_code("installed layout run without clang", &run, 3);
|
|
assert_stderr_contains(
|
|
"installed layout run without clang",
|
|
&run,
|
|
"ToolchainUnavailable",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn new_library_template_creates_checkable_testable_library_project() {
|
|
let project = unique_path("library-template");
|
|
|
|
let output = run_glagol([
|
|
"new".as_ref(),
|
|
project.as_os_str(),
|
|
"--template".as_ref(),
|
|
"library".as_ref(),
|
|
"--name".as_ref(),
|
|
"numbers".as_ref(),
|
|
]);
|
|
|
|
assert_success("glagol new --template library", &output);
|
|
assert_eq!(
|
|
fs::read_to_string(project.join("slovo.toml")).expect("read library manifest"),
|
|
"[project]\nname = \"numbers\"\nsource_root = \"src\"\nentry = \"lib\"\n"
|
|
);
|
|
let source = fs::read_to_string(project.join("src/lib.slo")).expect("read library source");
|
|
assert!(source.contains("(module lib (export answer double))"));
|
|
|
|
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
|
assert_success("library template check", &check);
|
|
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
|
assert_success("library template test", &test);
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&test.stdout),
|
|
"test \"answer is stable\" ... ok\ntest \"double works\" ... ok\n2 test(s) passed\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn new_workspace_template_creates_local_package_workspace() {
|
|
let workspace = unique_path("workspace-template");
|
|
|
|
let output = run_glagol([
|
|
"new".as_ref(),
|
|
workspace.as_os_str(),
|
|
"--template".as_ref(),
|
|
"workspace".as_ref(),
|
|
]);
|
|
|
|
assert_success("glagol new --template workspace", &output);
|
|
assert_eq!(
|
|
fs::read_to_string(workspace.join("slovo.toml")).expect("read workspace manifest"),
|
|
"[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\ndefault_package = \"app\"\n"
|
|
);
|
|
assert!(workspace.join("packages/app/slovo.toml").is_file());
|
|
assert!(workspace.join("packages/libutil/slovo.toml").is_file());
|
|
|
|
let check = run_glagol(["check".as_ref(), workspace.as_os_str()]);
|
|
assert_success("workspace template check", &check);
|
|
let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
|
|
assert_success("workspace template test", &test);
|
|
let stdout = String::from_utf8_lossy(&test.stdout);
|
|
assert!(stdout.contains("test \"app uses libutil\" ... ok"));
|
|
assert!(stdout.contains("test \"answer is stable\" ... ok"));
|
|
assert!(stdout.contains("2 test(s) passed"));
|
|
}
|
|
|
|
#[test]
|
|
fn new_rejects_non_empty_target_with_structured_diagnostic() {
|
|
let project = unique_path("new-non-empty");
|
|
fs::create_dir_all(&project).expect("create project dir");
|
|
fs::write(project.join("existing"), "").expect("write existing file");
|
|
|
|
let output = run_glagol(["new".as_ref(), project.as_os_str()]);
|
|
|
|
assert_exit_code("new non-empty", &output, 1);
|
|
assert!(output.stdout.is_empty(), "new failure wrote stdout");
|
|
assert_stderr_contains("new non-empty", &output, "ProjectScaffoldBlocked");
|
|
assert_stderr_contains("new non-empty", &output, "(schema slovo.diagnostic)");
|
|
}
|
|
|
|
#[test]
|
|
fn fmt_check_and_write_work_for_files() {
|
|
let fixture = write_file("fmt-file", "(module main)\n(fn main() -> i32 0)\n");
|
|
|
|
let check = run_glagol(["fmt".as_ref(), "--check".as_ref(), fixture.as_os_str()]);
|
|
assert_exit_code("fmt check dirty file", &check, 1);
|
|
assert_stderr_contains("fmt check dirty file", &check, "FormatCheckFailed");
|
|
|
|
let write = run_glagol(["fmt".as_ref(), "--write".as_ref(), fixture.as_os_str()]);
|
|
assert_success("fmt write file", &write);
|
|
assert!(write.stdout.is_empty(), "fmt --write wrote stdout");
|
|
assert_eq!(
|
|
fs::read_to_string(&fixture).expect("read formatted file"),
|
|
"(module main)\n\n(fn main () -> i32\n 0)\n"
|
|
);
|
|
|
|
let clean = run_glagol(["fmt".as_ref(), "--check".as_ref(), fixture.as_os_str()]);
|
|
assert_success("fmt check clean file", &clean);
|
|
}
|
|
|
|
#[test]
|
|
fn fmt_check_and_write_work_for_projects_deterministically() {
|
|
let project = write_project(
|
|
"fmt-project",
|
|
&[(
|
|
"math",
|
|
"(module math (export one))\n(fn one() -> i32 1)\n",
|
|
)],
|
|
"(module main)\n(import math (one))\n(fn main() -> i32 (one))\n",
|
|
);
|
|
|
|
let check = run_glagol(["fmt".as_ref(), "--check".as_ref(), project.as_os_str()]);
|
|
assert_exit_code("fmt check dirty project", &check, 1);
|
|
assert_stderr_contains("fmt check dirty project", &check, "FormatCheckFailed");
|
|
|
|
let write = run_glagol(["fmt".as_ref(), "--write".as_ref(), project.as_os_str()]);
|
|
assert_success("fmt write project", &write);
|
|
assert!(write.stdout.is_empty(), "project fmt --write wrote stdout");
|
|
assert_eq!(
|
|
fs::read_to_string(project.join("src/math.slo")).expect("read math"),
|
|
"(module math (export one))\n\n(fn one () -> i32\n 1)\n"
|
|
);
|
|
assert_eq!(
|
|
fs::read_to_string(project.join("src/main.slo")).expect("read main"),
|
|
"(module main)\n\n(import math (one))\n\n(fn main () -> i32\n (one))\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn doc_generates_markdown_for_file_and_project() {
|
|
let fixture = write_file(
|
|
"doc-file",
|
|
"(module docs)\n\n(struct Point (x i32))\n\n(fn main () -> i32\n 0)\n\n(test \"zero\"\n (= 0 0))\n",
|
|
);
|
|
let file_docs = unique_path("doc-file-out");
|
|
|
|
let file_output = run_glagol([
|
|
"doc".as_ref(),
|
|
fixture.as_os_str(),
|
|
"-o".as_ref(),
|
|
file_docs.as_os_str(),
|
|
]);
|
|
assert_success("doc file", &file_output);
|
|
let file_index = fs::read_to_string(file_docs.join("index.md")).expect("read file docs");
|
|
assert!(file_index.contains("# File "));
|
|
assert!(file_index.contains("## Module docs"));
|
|
assert!(file_index.contains("- `Point`"));
|
|
assert!(file_index.contains("- `main() -> i32`"));
|
|
assert!(file_index.contains("- `zero`"));
|
|
|
|
let project = write_project(
|
|
"doc-project",
|
|
&[(
|
|
"math",
|
|
"(module math (export one))\n\n(fn one () -> i32\n 1)\n",
|
|
)],
|
|
"(module main)\n\n(import math (one))\n\n(fn main () -> i32\n (one))\n",
|
|
);
|
|
let project_docs = unique_path("doc-project-out");
|
|
let project_output = run_glagol([
|
|
"doc".as_ref(),
|
|
project.as_os_str(),
|
|
"-o".as_ref(),
|
|
project_docs.as_os_str(),
|
|
]);
|
|
assert_success("doc project", &project_output);
|
|
let project_index =
|
|
fs::read_to_string(project_docs.join("index.md")).expect("read project docs");
|
|
assert!(project_index.contains("# Project doc-project"));
|
|
assert!(project_index.contains("## Module math"));
|
|
assert!(project_index.contains("## Module main"));
|
|
assert!(project_index.contains("- `math`"));
|
|
}
|
|
|
|
#[test]
|
|
fn doc_generates_workspace_package_summary() {
|
|
let workspace = unique_path("doc-workspace");
|
|
let new_output = run_glagol([
|
|
"new".as_ref(),
|
|
workspace.as_os_str(),
|
|
"--template".as_ref(),
|
|
"workspace".as_ref(),
|
|
]);
|
|
assert_success("doc workspace scaffold", &new_output);
|
|
|
|
let workspace_docs = unique_path("doc-workspace-out");
|
|
let doc_output = run_glagol([
|
|
"doc".as_ref(),
|
|
workspace.as_os_str(),
|
|
"-o".as_ref(),
|
|
workspace_docs.as_os_str(),
|
|
]);
|
|
assert_success("doc workspace", &doc_output);
|
|
|
|
let index = fs::read_to_string(workspace_docs.join("index.md")).expect("read workspace docs");
|
|
assert!(index.contains("## Workspace"));
|
|
assert!(index.contains("### Members"));
|
|
assert!(index.contains("- `packages/app`"));
|
|
assert!(index.contains("- `packages/libutil`"));
|
|
assert!(index.contains("### Packages"));
|
|
assert!(index.contains("- `app 0.1.0 (entry main)`"));
|
|
assert!(index.contains("- `libutil 0.1.0 (entry main)`"));
|
|
assert!(index.contains("### Package Dependencies"));
|
|
assert!(index.contains("- `app -> libutil (local-path, packages/libutil)`"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn project_tooling_rejects_symlinked_module_escape() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let project = write_project(
|
|
"tooling-escape",
|
|
&[],
|
|
"(module main)\n\n(fn main () -> i32\n 0)\n",
|
|
);
|
|
let outside = write_file(
|
|
"outside-module",
|
|
"(module escape)\n\n(fn value () -> i32\n 1)\n",
|
|
);
|
|
symlink(&outside, project.join("src/escape.slo")).expect("create module symlink");
|
|
|
|
let output = run_glagol(["fmt".as_ref(), "--check".as_ref(), project.as_os_str()]);
|
|
|
|
assert_exit_code("fmt check symlink escape", &output, 1);
|
|
assert_stderr_contains(
|
|
"fmt check symlink escape",
|
|
&output,
|
|
"ProjectManifestInvalid",
|
|
);
|
|
assert_stderr_contains(
|
|
"fmt check symlink escape",
|
|
&output,
|
|
"escapes the source root",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn release_gate_script_exists_and_names_required_commands() {
|
|
let script = Path::new("../scripts/release-gate.sh");
|
|
let text = fs::read_to_string(script).expect("read release gate script");
|
|
assert!(text.contains("git diff --check"));
|
|
assert!(text.contains("scripts/install.sh"));
|
|
assert!(text.contains("cargo fmt --check"));
|
|
assert!(text.contains("cargo test"));
|
|
assert!(text.contains("dx_v1_7"));
|
|
assert!(text.contains("beta_1_0_0"));
|
|
assert!(text.contains("beta_v2_0_0_beta_1"));
|
|
assert!(text.contains("promotion_gate"));
|
|
assert!(text.contains("binary_smoke"));
|
|
assert!(text.contains("llvm_smoke"));
|
|
}
|
|
|
|
fn write_project(name: &str, modules: &[(&str, &str)], main: &str) -> PathBuf {
|
|
let project = unique_path(name);
|
|
fs::create_dir_all(project.join("src")).expect("create project src");
|
|
fs::write(
|
|
project.join("slovo.toml"),
|
|
format!(
|
|
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
name
|
|
),
|
|
)
|
|
.expect("write manifest");
|
|
for (module, source) in modules {
|
|
fs::write(project.join("src").join(format!("{}.slo", module)), source)
|
|
.expect("write module");
|
|
}
|
|
fs::write(project.join("src/main.slo"), main).expect("write main");
|
|
project
|
|
}
|
|
|
|
fn write_file(name: &str, source: &str) -> PathBuf {
|
|
let path = unique_path(name).with_extension("slo");
|
|
fs::write(&path, source).expect("write fixture");
|
|
path
|
|
}
|
|
|
|
fn unique_path(name: &str) -> PathBuf {
|
|
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
|
|
let nanos = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("system clock before UNIX_EPOCH")
|
|
.as_nanos();
|
|
std::env::temp_dir().join(format!(
|
|
"glagol-dx-{}-{}-{}-{}",
|
|
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 run_installed_glagol<I, S>(binary: &Path, args: I) -> Output
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<std::ffi::OsStr>,
|
|
{
|
|
Command::new(binary)
|
|
.env_remove("SLOVO_STD_PATH")
|
|
.env_remove("SLOVO_RUNTIME_C")
|
|
.env_remove("GLAGOL_RUNTIME_C")
|
|
.args(args)
|
|
.output()
|
|
.expect("run installed glagol")
|
|
}
|
|
|
|
fn host_clang_available() -> bool {
|
|
let clang = std::env::var("GLAGOL_CLANG").unwrap_or_else(|_| "clang".to_string());
|
|
Command::new(clang)
|
|
.arg("--version")
|
|
.output()
|
|
.map(|output| output.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn make_executable(path: &Path) {
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mut permissions = fs::metadata(path).expect("stat executable").permissions();
|
|
permissions.set_mode(0o755);
|
|
fs::set_permissions(path, permissions).expect("chmod executable");
|
|
}
|
|
}
|
|
|
|
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_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
|
|
);
|
|
}
|