use std::{ fs, path::PathBuf, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, time::{SystemTime, UNIX_EPOCH}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn file_list_preserves_order_and_does_not_execute_bodies() { let fixture = write_fixture( "file-list", r#" (module main) (test "alpha first" true) (test "beta would fail" false) "#, ); let output = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]); assert_success_stdout( "file test list", output, "test \"alpha first\" ... selected\n\ test \"beta would fail\" ... selected\n\ 2 test(s) selected (total_discovered 2, selected 2, passed 0, failed 0, skipped 0, filter none)\n", ); } #[test] fn file_list_filter_marks_selected_and_skipped_in_order() { let fixture = write_fixture( "file-list-filter", r#" (module main) (test "alpha first" false) (test "beta second" false) "#, ); let output = run_glagol([ "test".as_ref(), "--list".as_ref(), fixture.as_os_str(), "--filter".as_ref(), "beta".as_ref(), ]); assert_success_stdout( "file filtered test list", output, "test \"alpha first\" ... skipped\n\ test \"beta second\" ... selected\n\ 1 test(s) selected (total_discovered 2, selected 1, passed 0, failed 0, skipped 1, filter \"beta\")\n", ); } #[test] fn legacy_run_tests_list_matches_test_subcommand_list() { let fixture = write_fixture( "legacy-list", r#" (module main) (test "legacy first" false) (test "legacy second" false) "#, ); let subcommand = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]); let legacy = run_glagol([ "--run-tests".as_ref(), "--list".as_ref(), fixture.as_os_str(), ]); assert_success("test --list", &subcommand); assert_success("legacy --run-tests --list", &legacy); assert_eq!( legacy.stdout, subcommand.stdout, "legacy list output differed from test subcommand" ); assert!(legacy.stderr.is_empty(), "legacy list wrote stderr"); } #[test] fn project_list_preserves_topological_order_and_filter_counts() { let project = write_project( "project-list", &[( "provider", "(module provider (export value))\n\n\ (fn value () -> i32\n 1)\n\n\ (test \"provider first\"\n false)\n", )], "(module main)\n\n\ (import provider (value))\n\n\ (fn main () -> i32\n (value))\n\n\ (test \"consumer second\"\n false)\n", ); let output = run_glagol([ "test".as_ref(), "--list".as_ref(), project.as_os_str(), "--filter".as_ref(), "consumer".as_ref(), ]); assert_success_stdout( "project filtered list", output, "test \"provider first\" ... skipped\n\ test \"consumer second\" ... selected\n\ 1 test(s) selected (total_discovered 2, selected 1, passed 0, failed 0, skipped 1, filter \"consumer\")\n", ); } #[test] fn workspace_list_preserves_package_order_without_running_tests() { let workspace = write_workspace( "workspace-list", "[workspace]\nmembers = [\"packages/app\", \"packages/util\"]\n", &[ WorkspacePackageSpec { member: "packages/util", manifest: "[package]\nname = \"util\"\nversion = \"0.1.0\"\n", modules: &[( "util", "(module util (export answer))\n\n\ (fn answer () -> i32\n 41)\n\n\ (test \"util provider first\"\n false)\n", )], }, WorkspacePackageSpec { member: "packages/app", manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nutil = { path = \"../util\" }\n", modules: &[( "main", "(module main)\n\n\ (import util.util (answer))\n\n\ (fn main () -> i32\n (answer))\n\n\ (test \"app consumer second\"\n false)\n", )], }, ], ); let output = run_glagol(["test".as_ref(), "--list".as_ref(), workspace.as_os_str()]); assert_success_stdout( "workspace list", output, "test \"util provider first\" ... selected\n\ test \"app consumer second\" ... selected\n\ 2 test(s) selected (total_discovered 2, selected 2, passed 0, failed 0, skipped 0, filter none)\n", ); } #[test] fn ordinary_test_output_stays_byte_stable() { let fixture = write_fixture( "ordinary-test", r#" (module main) (test "alpha first" true) (test "beta second" true) "#, ); let subcommand = run_glagol(["test".as_ref(), fixture.as_os_str()]); assert_success_stdout( "ordinary test", subcommand, "test \"alpha first\" ... ok\n\ test \"beta second\" ... ok\n\ 2 test(s) passed\n", ); let legacy = run_glagol(["--run-tests".as_ref(), fixture.as_os_str()]); assert_success_stdout( "ordinary legacy run-tests", legacy, "test \"alpha first\" ... ok\n\ test \"beta second\" ... ok\n\ 2 test(s) passed\n", ); } #[test] fn list_mode_reuses_checked_diagnostics_for_invalid_tests() { let fixture = write_fixture( "invalid-list", r#" (module main) (test "not bool" 1) "#, ); let output = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]); assert_failure_stderr_contains( "invalid list", &output, &[ "TestExpressionNotBool", "Expected:", "bool", "Found:", "i32", ], ); assert!( output.stdout.is_empty(), "invalid list wrote stdout:\n{}", String::from_utf8_lossy(&output.stdout) ); } #[test] fn list_flag_is_rejected_outside_test_mode() { let fixture = write_fixture( "list-outside-test", r#" (module main) (fn main () -> i32 0) "#, ); let output = run_glagol(["check".as_ref(), "--list".as_ref(), fixture.as_os_str()]); assert_failure_stderr_contains( "check --list", &output, &["`--list` is only supported with `test` and `--run-tests`"], ); } fn write_fixture(name: &str, source: &str) -> PathBuf { let path = unique_path(name).with_extension("slo"); fs::write(&path, source).expect("write fixture"); path } fn write_project(name: &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"), format!( "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n", name ), ) .expect("write project manifest"); fs::write(src.join("main.slo"), main).expect("write project main"); for (module, source) in modules { fs::write(src.join(format!("{}.slo", module)), source).expect("write project 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 workspace package src"); fs::write(package_root.join("slovo.toml"), package.manifest) .expect("write workspace package manifest"); for (module, source) in package.modules { fs::write(src.join(format!("{}.slo", module)), source) .expect("write workspace package module"); } } root } fn unique_path(name: &str) -> PathBuf { let id = NEXT_FIXTURE_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-test-discovery-beta19-{}-{}-{}-{}", 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) ); } 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_failure_stderr_contains(context: &str, output: &Output, expected: &[&str]) { assert!( !output.status.success(), "{} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}", context, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let stderr = String::from_utf8_lossy(&output.stderr); for needle in expected { assert!( stderr.contains(needle), "{} stderr missing `{}`:\n{}", context, needle, stderr ); } }