use std::{ fs, path::PathBuf, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn symbols_single_file_reports_editor_facing_source_metadata() { let source = r#"(module docs (export main)) (type Count i32) (struct Point (x i32) (y i32)) (enum Status Ready (Blocked i32)) (fn main () -> i32 0) (test "main returns zero" (= (main) 0)) "#; let file = write_file("symbols-file", source); let output = run_glagol(["symbols".as_ref(), file.as_os_str()]); assert_success("symbols file", &output); assert!(output.stderr.is_empty(), "symbols wrote stderr"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.starts_with("(symbols\n")); assert!(stdout.contains(" (schema slovo.symbols)\n")); assert!(stdout.contains(" (version \"1.0.0-beta.10\")\n")); assert!(stdout.contains(&format!(" (path \"{}\")\n", file.display()))); assert!(stdout.contains(" (name \"docs\")\n")); assert!(stdout.contains(" (module_span (span ")); assert!(stdout.contains("(range (start (line 1) (column 1))")); assert!(stdout.contains(" (name \"main\")\n")); assert!(stdout.contains(" (name \"Count\")\n")); assert!(stdout.contains(" (name \"Point\")\n")); assert!(stdout.contains(" (name \"Status\")\n")); assert!(stdout.contains(" (name \"Blocked\")\n")); assert!(stdout.contains(" (name \"main returns zero\")\n")); } #[test] fn symbols_project_writes_output_and_manifest_without_stdout() { let project = write_project( "symbols-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 output_path = unique_path("symbols-project-out").with_extension("sexpr"); let manifest_path = unique_path("symbols-project-manifest").with_extension("slo"); let output = run_glagol([ "symbols".as_ref(), project.as_os_str(), "-o".as_ref(), output_path.as_os_str(), "--manifest".as_ref(), manifest_path.as_os_str(), ]); assert_success("symbols project", &output); assert!(output.stdout.is_empty(), "symbols -o wrote stdout"); assert!(output.stderr.is_empty(), "symbols project wrote stderr"); let symbols = fs::read_to_string(&output_path).expect("read symbols output"); assert!(symbols.contains(" (name \"main\")\n")); assert!(symbols.contains(" (name \"math\")\n")); assert!(symbols.contains(" (module \"math\")\n")); assert!(symbols.contains(" (name \"one\")\n")); assert!(symbols.contains(" (imports\n")); assert!(symbols.contains(" (functions\n")); let manifest = fs::read_to_string(&manifest_path).expect("read symbols manifest"); assert!(manifest.contains(" (mode symbols)\n")); assert!(manifest.contains(" (kind symbols)\n")); assert!(manifest.contains(&format!(" (path \"{}\")\n", output_path.display()))); } #[test] fn symbols_workspace_includes_package_names_deterministically() { let workspace = unique_path("symbols-workspace"); let scaffold = run_glagol([ "new".as_ref(), workspace.as_os_str(), "--template".as_ref(), "workspace".as_ref(), ]); assert_success("workspace scaffold", &scaffold); let first = run_glagol(["symbols".as_ref(), workspace.as_os_str()]); let second = run_glagol(["symbols".as_ref(), workspace.as_os_str()]); assert_success("symbols workspace first", &first); assert_success("symbols workspace second", &second); assert_eq!( first.stdout, second.stdout, "workspace symbols output was not deterministic" ); let stdout = String::from_utf8_lossy(&first.stdout); assert!(stdout.contains(" (package \"app\")\n")); assert!(stdout.contains(" (package \"libutil\")\n")); assert!(stdout.contains(" (module \"libutil.libutil\")\n")); } #[test] fn symbols_usage_is_part_of_the_public_cli_surface() { let output = run_glagol([std::ffi::OsStr::new("--help")]); assert_success("glagol help", &output); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("glagol symbols ")); } 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-symbols-beta10-{}-{}-{}-{}", 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 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) ); }