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(args: I) -> Output where I: IntoIterator, S: AsRef, { Command::new(env!("CARGO_BIN_EXE_glagol")) .args(args) .output() .expect("run glagol") } fn run_installed_glagol(binary: &Path, args: I) -> Output where I: IntoIterator, S: AsRef, { 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 ); }