use std::{ fs, path::PathBuf, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, time::{SystemTime, UNIX_EPOCH}, }; static NEXT_WORKSPACE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn duplicate_package_keys_report_package_manifest_invalid() { let workspace = write_workspace( "duplicate-package-key", "[workspace]\nmembers = [\"packages/app\"]\n", &[WorkspacePackageSpec { member: "packages/app", manifest: "[package]\nname = \"app\"\nname = \"other\"\nversion = \"0.1.0\"\n", modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")], }], ); let output = run_glagol([ "--json-diagnostics".as_ref(), "check".as_ref(), workspace.as_os_str(), ]); assert_exit_code("duplicate package key", &output, 1); assert_json_diagnostic_code("duplicate package key", &output, "PackageManifestInvalid"); assert_json_diagnostic_code_absent("duplicate package key", &output, "ProjectManifestInvalid"); } #[test] fn invalid_dependency_key_reports_invalid_package_dependency_name() { let workspace = write_workspace( "invalid-dependency-key", "[workspace]\nmembers = [\"packages/app\"]\n", &[WorkspacePackageSpec { member: "packages/app", manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nBad_Name = { path = \"../util\" }\n", modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")], }], ); let output = run_glagol([ "--json-diagnostics".as_ref(), "check".as_ref(), workspace.as_os_str(), ]); assert_exit_code("invalid dependency key", &output, 1); assert_json_diagnostic_code( "invalid dependency key", &output, "InvalidPackageDependencyName", ); } #[test] fn duplicate_dependency_keys_report_duplicate_package_dependency_name() { let workspace = write_workspace( "duplicate-dependency-key", "[workspace]\nmembers = [\"packages/app\"]\n", &[WorkspacePackageSpec { member: "packages/app", manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nutil = { path = \"../util\" }\nutil = { path = \"../util-again\" }\n", modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")], }], ); let output = run_glagol([ "--json-diagnostics".as_ref(), "check".as_ref(), workspace.as_os_str(), ]); assert_exit_code("duplicate dependency key", &output, 1); assert_json_diagnostic_code( "duplicate dependency key", &output, "DuplicatePackageDependencyName", ); } #[test] fn valid_dependency_identity_checks_cleanly() { let workspace = write_workspace( "valid-dependency-identity", "[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 42)\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", )], }, ], ); let output = run_glagol(["check".as_ref(), workspace.as_os_str()]); assert_success_stdout("valid dependency identity", output, ""); } 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_WORKSPACE_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-package-workspace-discipline-beta24-{}-{}-{}-{}", 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_stdout(context: &str, output: Output, expected: &str) { assert!( output.status.success(), "{} failed\nstdout:\n{}\nstderr:\n{}", context, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); 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_json_diagnostic_code(context: &str, output: &Output, expected: &str) { let diagnostics = diagnostic_text(output); assert!( diagnostics.contains(&format!(r#""code":"{}""#, expected)), "{} did not report `{}`:\n{}", context, expected, diagnostics ); } fn assert_json_diagnostic_code_absent(context: &str, output: &Output, unexpected: &str) { let diagnostics = diagnostic_text(output); assert!( !diagnostics.contains(&format!(r#""code":"{}""#, unexpected)), "{} unexpectedly reported `{}`:\n{}", context, unexpected, diagnostics ); } fn diagnostic_text(output: &Output) -> String { format!( "{}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ) }