Files
storybook/src/resolve/prop_tests.rs
Sienna Meridian Satterwhite 16deb5d237 release: Storybook v0.2.0 - Major syntax and features update
BREAKING CHANGES:
- Relationship syntax now requires blocks for all participants
- Removed self/other perspective blocks from relationships
- Replaced 'guard' keyword with 'if' for behavior tree decorators

Language Features:
- Add tree-sitter grammar with improved if/condition disambiguation
- Add comprehensive tutorial and reference documentation
- Add SBIR v0.2.0 binary format specification
- Add resource linking system for behaviors and schedules
- Add year-long schedule patterns (day, season, recurrence)
- Add behavior tree enhancements (named nodes, decorators)

Documentation:
- Complete tutorial series (9 chapters) with baker family examples
- Complete reference documentation for all language features
- SBIR v0.2.0 specification with binary format details
- Added locations and institutions documentation

Examples:
- Convert all examples to baker family scenario
- Add comprehensive working examples

Tooling:
- Zed extension with LSP integration
- Tree-sitter grammar for syntax highlighting
- Build scripts and development tools

Version Updates:
- Main package: 0.1.0 → 0.2.0
- Tree-sitter grammar: 0.1.0 → 0.2.0
- Zed extension: 0.1.0 → 0.2.0
- Storybook editor: 0.1.0 → 0.2.0
2026-02-13 21:52:03 +00:00

318 lines
9.4 KiB
Rust

//! Property-based tests for the resolution engine
use proptest::prelude::*;
use crate::{
resolve::names::{
DeclKind,
NameTable,
},
syntax::ast::*,
};
// ===== Generators =====
fn valid_ident() -> impl Strategy<Value = String> {
"[a-zA-Z_][a-zA-Z0-9_]{0,20}".prop_filter("not a keyword", |s| {
!matches!(
s.as_str(),
"use" |
"character" |
"template" |
"life_arc" |
"schedule" |
"behavior" |
"institution" |
"relationship" |
"location" |
"species" |
"enum" |
"state" |
"on" |
"as" |
"self" |
"other" |
"remove" |
"append" |
"forall" |
"exists" |
"in" |
"where" |
"and" |
"or" |
"not" |
"is" |
"true" |
"false"
)
})
}
fn valid_character_decl() -> impl Strategy<Value = (String, Declaration)> {
valid_ident().prop_map(|name| {
let decl = Declaration::Character(Character {
name: name.clone(),
species: None,
fields: vec![],
template: None,
uses_behaviors: None,
uses_schedule: None,
span: Span::new(0, 10),
});
(name, decl)
})
}
fn valid_template_decl() -> impl Strategy<Value = (String, Declaration)> {
valid_ident().prop_map(|name| {
let decl = Declaration::Template(Template {
name: name.clone(),
fields: vec![],
strict: false,
includes: vec![],
uses_behaviors: None,
uses_schedule: None,
span: Span::new(0, 10),
});
(name, decl)
})
}
fn valid_enum_decl() -> impl Strategy<Value = (String, Declaration)> {
(valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(name, variants)| {
let decl = Declaration::Enum(EnumDecl {
name: name.clone(),
variants,
span: Span::new(0, 10),
});
(name, decl)
})
}
fn valid_use_single() -> impl Strategy<Value = Declaration> {
(valid_ident(), valid_ident()).prop_map(|(module, name)| {
Declaration::Use(UseDecl {
path: vec![module, name],
kind: UseKind::Single,
span: Span::new(0, 10),
})
})
}
fn valid_use_grouped() -> impl Strategy<Value = Declaration> {
(valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(module, names)| {
Declaration::Use(UseDecl {
path: vec![module],
kind: UseKind::Grouped(names),
span: Span::new(0, 10),
})
})
}
fn valid_use_wildcard() -> impl Strategy<Value = Declaration> {
valid_ident().prop_map(|module| {
Declaration::Use(UseDecl {
path: vec![module],
kind: UseKind::Wildcard,
span: Span::new(0, 10),
})
})
}
// ===== Property Tests =====
proptest! {
#[test]
fn test_name_table_registers_all_declarations(
chars in prop::collection::vec(valid_character_decl(), 0..10)
) {
let declarations: Vec<_> = chars.iter().map(|(_, decl)| decl.clone()).collect();
let file = File { declarations };
let result = NameTable::from_file(&file);
if chars.is_empty() {
// Empty file should succeed
assert!(result.is_ok());
} else {
// Check if there are duplicates
let mut seen = std::collections::HashSet::new();
let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name));
if has_duplicates {
// Should fail with duplicate error
assert!(result.is_err());
} else {
// Should succeed and all names should be registered
let table = result.unwrap();
for (name, _) in &chars {
assert!(table.lookup(std::slice::from_ref(name)).is_some(),
"Name '{}' should be registered", name);
}
}
}
}
#[test]
fn test_duplicate_detection_always_fails(
name in valid_ident(),
count in 2usize..5
) {
let declarations: Vec<_> = (0..count).map(|i| {
Declaration::Character(Character {
name: name.clone(),
species: None,
fields: vec![],
template: None,
uses_behaviors: None,
uses_schedule: None,
span: Span::new(i * 10, i * 10 + 10),
})
}).collect();
let file = File { declarations };
let result = NameTable::from_file(&file);
// Should always fail with duplicate error
assert!(result.is_err());
}
#[test]
fn test_lookup_is_case_sensitive(name in valid_ident()) {
let file = File {
declarations: vec![
Declaration::Character(Character {
name: name.clone(),
species: None,
fields: vec![],
template: None,
uses_behaviors: None,
uses_schedule: None,
span: Span::new(0, 10),
}),
],
};
let table = NameTable::from_file(&file).unwrap();
// Original name should be found
assert!(table.lookup(std::slice::from_ref(&name)).is_some());
// Different case should not be found
let uppercase = name.to_uppercase();
if uppercase != name {
assert!(table.lookup(&[uppercase]).is_none());
}
}
#[test]
fn test_kind_filtering_works(
chars in prop::collection::vec(valid_character_decl(), 0..5),
templates in prop::collection::vec(valid_template_decl(), 0..5),
enums in prop::collection::vec(valid_enum_decl(), 0..5)
) {
let mut declarations = vec![];
declarations.extend(chars.iter().map(|(_, d)| d.clone()));
declarations.extend(templates.iter().map(|(_, d)| d.clone()));
declarations.extend(enums.iter().map(|(_, d)| d.clone()));
let file = File { declarations };
// Only proceed if no duplicates
let mut seen = std::collections::HashSet::new();
let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name))
|| templates.iter().any(|(name, _)| !seen.insert(name))
|| enums.iter().any(|(name, _)| !seen.insert(name));
if !has_duplicates {
let table = NameTable::from_file(&file).unwrap();
let char_count = table.entries_of_kind(DeclKind::Character).count();
let template_count = table.entries_of_kind(DeclKind::Template).count();
let enum_count = table.entries_of_kind(DeclKind::Enum).count();
assert_eq!(char_count, chars.len());
assert_eq!(template_count, templates.len());
assert_eq!(enum_count, enums.len());
}
}
#[test]
fn test_use_statements_are_collected(
uses in prop::collection::vec(
prop_oneof![
valid_use_single(),
valid_use_grouped(),
valid_use_wildcard(),
],
0..10
)
) {
let file = File { declarations: uses.clone() };
let table = NameTable::from_file(&file).unwrap();
assert_eq!(table.imports().len(), uses.len());
}
#[test]
fn test_fuzzy_matching_finds_close_names(
name in valid_ident().prop_filter("long enough", |s| s.len() > 3)
) {
let file = File {
declarations: vec![
Declaration::Character(Character {
name: name.clone(),
species: None,
fields: vec![],
template: None,
uses_behaviors: None,
uses_schedule: None,
span: Span::new(0, 10),
}),
],
};
let table = NameTable::from_file(&file).unwrap();
// Create a typo by swapping two adjacent characters
if name.len() >= 2 {
let mut chars: Vec<char> = name.chars().collect();
chars.swap(0, 1);
let typo: String = chars.into_iter().collect();
// Should suggest the original name
if let Some(suggestion) = table.find_suggestion(&typo) {
assert_eq!(suggestion, name);
}
}
}
#[test]
fn test_mixed_declarations_and_imports(
chars in prop::collection::vec(valid_character_decl(), 1..5),
uses in prop::collection::vec(valid_use_single(), 0..3)
) {
let mut declarations = uses;
declarations.extend(chars.iter().map(|(_, d)| d.clone()));
let file = File { declarations };
// Check for duplicates
let mut seen = std::collections::HashSet::new();
let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name));
if !has_duplicates {
let table = NameTable::from_file(&file).unwrap();
// All characters should be registered
for (name, _) in &chars {
assert!(table.lookup(std::slice::from_ref(name)).is_some());
}
}
}
}