//! Integration tests for the CLI tool //! //! These tests verify that the `sb` command-line tool works correctly //! by testing it against real project files. use std::{ fs, path::PathBuf, process::Command, }; use tempfile::TempDir; /// Helper to get the path to the compiled sb binary fn sb_binary() -> PathBuf { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push("target"); path.push("debug"); path.push("sb"); path } /// Helper to create a temporary test project fn create_test_project() -> TempDir { let dir = TempDir::new().unwrap(); // Create a valid character file fs::write( dir.path().join("test.sb"), r#" character Martha { age: 34 trust: 0.8 } character David { age: 42 health: 0.9 } "#, ) .unwrap(); dir } /// Helper to create a project with errors fn create_invalid_project() -> TempDir { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("errors.sb"), r#" character Martha { age: 200 trust: 1.5 } "#, ) .unwrap(); dir } #[test] fn test_validate_valid_project() { let project = create_test_project(); let output = Command::new(sb_binary()) .arg("validate") .arg(project.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Validation should succeed for valid project. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 2")); } #[test] fn test_validate_invalid_project() { let project = create_invalid_project(); let output = Command::new(sb_binary()) .arg("validate") .arg(project.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Validation should fail for invalid project" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("Found 2 errors")); assert!(stderr.contains("Trait 'age' has value 200")); assert!(stderr.contains("Trait 'trust' has value 1.5")); } #[test] fn test_validate_single_file() { let project = create_test_project(); let file_path = project.path().join("test.sb"); let output = Command::new(sb_binary()) .arg("validate") .arg(&file_path) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Validation should succeed for valid file" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); } #[test] fn test_validate_nonexistent_path() { let output = Command::new(sb_binary()) .arg("validate") .arg("/nonexistent/path/to/project") .output() .expect("Failed to execute sb validate"); assert!(!output.status.success(), "Should fail for nonexistent path"); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("Path does not exist")); } #[test] fn test_inspect_character() { let project = create_test_project(); let output = Command::new(sb_binary()) .arg("inspect") .arg("Martha") .arg("--path") .arg(project.path()) .output() .expect("Failed to execute sb inspect"); assert!(output.status.success(), "Inspect should succeed"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Character: Martha")); assert!(stdout.contains("age")); assert!(stdout.contains("34")); } #[test] fn test_inspect_nonexistent_entity() { let project = create_test_project(); let output = Command::new(sb_binary()) .arg("inspect") .arg("NonExistent") .arg("--path") .arg(project.path()) .output() .expect("Failed to execute sb inspect"); assert!(output.status.success(), "Inspect runs even if not found"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("not found")); } #[test] fn test_validate_empty_project() { let dir = TempDir::new().unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!(!output.status.success(), "Should fail for empty project"); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("No .sb files found")); } #[test] fn test_validate_shows_multiple_errors() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("multi_error.sb"), r#" character Alice { age: 200 trust: 1.5 bond: -0.2 } character Bob { age: -10 love: 3.0 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Should fail with validation errors" ); let stderr = String::from_utf8_lossy(&output.stderr); // Should show all 5 errors (non-fail-fast) assert!(stderr.contains("Found 5 errors")); assert!(stderr.contains("age")); // age: 200 assert!(stderr.contains("trust")); // trust: 1.5 assert!(stderr.contains("bond")); // bond: -0.2 assert!(stderr.contains("-10")); // age: -10 assert!(stderr.contains("love")); // love: 3.0 } #[test] fn test_cross_file_name_resolution() { let dir = TempDir::new().unwrap(); // Create multiple files with characters fs::write( dir.path().join("file1.sb"), r#" character Martha { age: 34 trust: 0.8 } "#, ) .unwrap(); fs::write( dir.path().join("file2.sb"), r#" character David { age: 42 health: 0.9 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Should successfully load and validate multiple files" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 2")); } #[test] fn test_cross_file_duplicate_detection() { let dir = TempDir::new().unwrap(); // Create two files with the same character name fs::write( dir.path().join("file1.sb"), r#" character Martha { age: 34 } "#, ) .unwrap(); fs::write( dir.path().join("file2.sb"), r#" character Martha { age: 42 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Should fail with duplicate definition error" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("Duplicate definition of 'Martha'")); } // ===== Template Composition Tests ===== #[test] fn test_template_composition_with_includes() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("templates.sb"), r#" // Base template template Being { alive: true } // Template that includes Being template Human { include Being kind: "human" } // Character with template composition character Martha from Human { firstName: "Martha" age: 34 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Template composition with includes should succeed" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful!")); assert!(stdout.contains("Characters: 1")); } #[test] fn test_template_composition_multiple_templates() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("templates.sb"), r#" template Physical { height: 0 weight: 0 } template Mental { iq: 0 } character David from Physical, Mental { height: 180 weight: 75 iq: 120 firstName: "David" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Multiple template inheritance should succeed" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful!")); } #[test] fn test_strict_template_validation_success() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("strict.sb"), r#" template Person strict { age: 18..100 firstName: "" } character Martha from Person { age: 34 firstName: "Martha" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Strict template with concrete values should succeed" ); } #[test] fn test_strict_template_validation_failure() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("strict_fail.sb"), r#" template Person strict { age: 18..100 } character Martha from Person { firstName: "Martha" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Strict template with range value should fail" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("strict template")); assert!(stderr.contains("range value")); } #[test] fn test_template_chained_includes() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("chained.sb"), r#" template Being { alive: true } template Human { include Being kind: "human" } template Person strict { include Human age: 18..100 } character Martha from Person { age: 34 firstName: "Martha" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Chained template includes should succeed" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful!")); } #[test] fn test_reserved_keyword_field_name_fails_at_parse() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("reserved.sb"), r#" character Martha { species: "human" age: 34 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Field with reserved keyword name should fail at parse time" ); let stderr = String::from_utf8_lossy(&output.stderr); // Parser catches this as UnrecognizedToken before validation assert!(stderr.contains("Parse error") || stderr.contains("UnrecognizedToken")); }