547 lines
12 KiB
Rust
547 lines
12 KiB
Rust
|
|
//! 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"));
|
||
|
|
}
|