// Workspace-level integration tests for diagnostics // Tests mlf-lang + mlf-diagnostics working together use mlf_diagnostics::{get_error_module_namespace_str, ValidationDiagnostic}; use mlf_integration_tests::test_utils; use mlf_lang::{parser::parse_lexicon, Workspace}; use serde::Deserialize; use std::fs; use std::path::Path; #[derive(Debug, Deserialize)] struct ExpectedDiagnostic { error_count: usize, errors: Vec, } #[derive(Debug, Deserialize)] struct ExpectedError { #[allow(dead_code)] code: String, message: String, #[serde(default)] #[allow(dead_code)] span: Option, } #[derive(Debug, Deserialize)] struct ExpectedSpan { #[allow(dead_code)] start: usize, #[allow(dead_code)] end: usize, } #[test] fn diagnostics_tests() { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let test_base = format!("{}/diagnostics", manifest_dir); let test_dirs = test_utils::discover_test_dirs(&test_base); let tests: Vec<(String, Result<(), String>)> = test_dirs .into_iter() .map(|test_dir| { let test_name = format!( "diagnostics/{}", Path::new(&test_dir).file_name().unwrap().to_str().unwrap() ); let result = run_diagnostics_test(&test_dir); (test_name, result) }) .collect(); let (_passed, failed) = test_utils::run_and_report_tests:: Result<(), String>>(tests, "Diagnostics"); if !failed.is_empty() { panic!( "\nFailed tests:\n{}", failed .iter() .map(|(name, err)| format!(" - {}: {}", name, err)) .collect::>() .join("\n") ); } } fn derive_namespace_fallback(test_name: &str) -> String { format!("test.{}", test_name) } fn run_diagnostics_test(test_dir: &str) -> Result<(), String> { // 1. Load test configuration let config = test_utils::load_test_config( Path::new(test_dir), |test_name| derive_namespace_fallback(test_name) )?; let namespace = config.test.namespace .ok_or_else(|| "No namespace specified in test.toml".to_string())?; // 2. Read input.mlf let input_path = format!("{}/input.mlf", test_dir); let input = fs::read_to_string(&input_path) .map_err(|e| format!("Failed to read input.mlf: {}", e))?; // 3. Parse with mlf-lang let lexicon = parse_lexicon(&input) .map_err(|e| format!("Failed to parse: {:?}", e))?; // 4. Create workspace and resolve (expect errors) let mut ws = Workspace::with_std() .map_err(|e| format!("Failed to create workspace: {:?}", e))?; ws.add_module(namespace.clone(), lexicon) .map_err(|e| format!("Failed to add module: {:?}", e))?; let validation_errors = match ws.resolve() { Ok(()) => return Err("Expected validation errors but got none".to_string()), Err(errors) => errors, }; // 5. Create diagnostic let _diagnostic = ValidationDiagnostic::new( "input.mlf".to_string(), input.clone(), namespace.clone(), validation_errors.clone(), ); // 6. Read expected output let expected_path = format!("{}/expected.json", test_dir); let expected_str = fs::read_to_string(&expected_path) .map_err(|e| format!("Failed to read expected.json: {}", e))?; let expected: ExpectedDiagnostic = serde_json::from_str(&expected_str) .map_err(|e| format!("Failed to parse expected.json: {}", e))?; // 7. Filter errors to this module let errors_in_module: Vec<_> = validation_errors .errors .iter() .filter(|e| get_error_module_namespace_str(e) == namespace) .collect(); // 8. Verify error count if errors_in_module.len() != expected.error_count { return Err(format!( "Expected {} errors but got {}", expected.error_count, errors_in_module.len() )); } // 9. Verify each error for (i, expected_error) in expected.errors.iter().enumerate() { if i >= errors_in_module.len() { return Err(format!("Expected error #{} but only got {} errors", i + 1, errors_in_module.len())); } let actual_error = errors_in_module[i]; // Check error code let _actual_code = mlf_diagnostics::get_error_module_namespace_str(actual_error); // Format the error message let actual_message = format!("{:?}", actual_error); // Verify message contains expected text if !actual_message.contains(&expected_error.message) { return Err(format!( "Error #{}: Expected message to contain '{}' but got: {}", i + 1, expected_error.message, actual_message )); } } Ok(()) }