This commit completes the migration started in the previous commit, updating all remaining files: - Lexer: Changed token from Extends to Modifies - Parser: Updated lalrpop grammar rules and AST field names - AST: Renamed Schedule.extends field to modifies - Grammar: Updated tree-sitter grammar.js - Tree-sitter: Regenerated parser.c and node-types.json - Examples: Updated baker-family work schedules - Tests: Updated schedule composition tests and corpus - Docs: Updated all reference documentation and tutorials - Validation: Updated error messages and validation logic - Package: Bumped version to 0.3.1 in all package manifests All 554 tests pass.
443 lines
13 KiB
Rust
443 lines
13 KiB
Rust
//! Tree-sitter Grammar Integration Tests
|
|
//!
|
|
//! These tests validate that:
|
|
//! 1. The tree-sitter grammar builds correctly
|
|
//! 2. All queries (highlights, injections, etc.) are valid
|
|
//! 3. Example files parse without errors
|
|
//! 4. The Zed extension builds successfully
|
|
//!
|
|
//! These are acceptance tests that catch integration issues between
|
|
//! the Storybook language and its tree-sitter representation.
|
|
|
|
use std::{
|
|
path::{
|
|
Path,
|
|
PathBuf,
|
|
},
|
|
process::Command,
|
|
};
|
|
|
|
fn tree_sitter_dir() -> PathBuf {
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
path.push("tree-sitter-storybook");
|
|
path
|
|
}
|
|
|
|
fn zed_extension_dir() -> PathBuf {
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
path.push("zed-storybook");
|
|
path
|
|
}
|
|
|
|
fn examples_dir() -> PathBuf {
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
path.push("examples");
|
|
path
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tree-sitter Grammar Build Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_tree_sitter_grammar_generates() {
|
|
let output = Command::new("tree-sitter")
|
|
.arg("generate")
|
|
.current_dir(tree_sitter_dir())
|
|
.output()
|
|
.expect("Failed to run tree-sitter generate. Is tree-sitter-cli installed?");
|
|
|
|
if !output.status.success() {
|
|
eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"tree-sitter generate failed. Grammar may have syntax errors."
|
|
);
|
|
|
|
// Verify generated files exist
|
|
let parser_c = tree_sitter_dir().join("src").join("parser.c");
|
|
assert!(
|
|
parser_c.exists(),
|
|
"parser.c not generated at {:?}",
|
|
parser_c
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tree_sitter_grammar_builds() {
|
|
let output = Command::new("tree-sitter")
|
|
.arg("generate")
|
|
.current_dir(tree_sitter_dir())
|
|
.output()
|
|
.expect("Failed to run tree-sitter generate");
|
|
|
|
if !output.status.success() {
|
|
eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"tree-sitter generate failed. Grammar may not compile."
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Query Validation Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_highlights_query_valid() {
|
|
let output = Command::new("tree-sitter")
|
|
.arg("test")
|
|
.current_dir(tree_sitter_dir())
|
|
.output()
|
|
.expect("Failed to run tree-sitter test");
|
|
|
|
if !output.status.success() {
|
|
eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"tree-sitter test failed. Queries (highlights.scm, etc.) may have errors."
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_queries_reference_valid_nodes() {
|
|
// This test verifies that all node types referenced in queries
|
|
// actually exist in the grammar. This catches issues like:
|
|
// - Using "any" instead of (any_type)
|
|
// - Referencing removed or renamed nodes
|
|
// - Typos in node names
|
|
|
|
let highlights_scm = tree_sitter_dir().join("queries").join("highlights.scm");
|
|
|
|
assert!(
|
|
highlights_scm.exists(),
|
|
"highlights.scm not found at {:?}",
|
|
highlights_scm
|
|
);
|
|
|
|
let content = std::fs::read_to_string(&highlights_scm).expect("Failed to read highlights.scm");
|
|
|
|
// Check for common mistakes
|
|
let problematic_patterns = vec![
|
|
(
|
|
"\"any\"",
|
|
"Should use (any_type) instead of \"any\" string literal",
|
|
),
|
|
(
|
|
"\"Number\"",
|
|
"Number is not a keyword, should use a type node",
|
|
),
|
|
(
|
|
"\"Decimal\"",
|
|
"Decimal is not a keyword, should use a type node",
|
|
),
|
|
("\"Text\"", "Text is not a keyword, should use a type node"),
|
|
(
|
|
"\"Boolean\"",
|
|
"Boolean is not a keyword, should use a type node",
|
|
),
|
|
];
|
|
|
|
for (pattern, msg) in problematic_patterns {
|
|
if content.contains(pattern) {
|
|
// Check if it's in a comment
|
|
let lines: Vec<&str> = content.lines().collect();
|
|
for line in lines {
|
|
if line.contains(pattern) && !line.trim_start().starts_with(';') {
|
|
panic!(
|
|
"highlights.scm contains problematic pattern '{}': {}",
|
|
pattern, msg
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example File Parsing Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_baker_family_example_parses() {
|
|
let baker_family = examples_dir().join("baker-family");
|
|
assert!(baker_family.exists(), "baker-family example not found");
|
|
|
|
// Find all .sb files
|
|
let sb_files = find_sb_files(&baker_family);
|
|
assert!(
|
|
!sb_files.is_empty(),
|
|
"No .sb files found in baker-family example"
|
|
);
|
|
|
|
for file in &sb_files {
|
|
let output = Command::new("tree-sitter")
|
|
.arg("parse")
|
|
.arg(file)
|
|
.current_dir(tree_sitter_dir())
|
|
.output()
|
|
.expect("Failed to run tree-sitter parse");
|
|
|
|
if !output.status.success() {
|
|
eprintln!("Failed to parse: {:?}", file);
|
|
eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"Failed to parse {:?} with tree-sitter",
|
|
file
|
|
);
|
|
|
|
// Check for ERROR nodes in the parse tree
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
if stdout.contains("ERROR") {
|
|
eprintln!("Parse tree for {:?}:", file);
|
|
eprintln!("{}", stdout);
|
|
panic!("Parse tree contains ERROR nodes for {:?}", file);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_all_examples_parse() {
|
|
let sb_files = find_sb_files(&examples_dir());
|
|
assert!(!sb_files.is_empty(), "No .sb files found in examples/");
|
|
|
|
let mut failures = Vec::new();
|
|
|
|
for file in &sb_files {
|
|
let output = Command::new("tree-sitter")
|
|
.arg("parse")
|
|
.arg(file)
|
|
.current_dir(tree_sitter_dir())
|
|
.output()
|
|
.expect("Failed to run tree-sitter parse");
|
|
|
|
if !output.status.success() {
|
|
failures.push((file.clone(), "Parse command failed".to_string()));
|
|
continue;
|
|
}
|
|
|
|
// Check for ERROR nodes
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
if stdout.contains("ERROR") {
|
|
failures.push((file.clone(), "Parse tree contains ERROR nodes".to_string()));
|
|
}
|
|
}
|
|
|
|
if !failures.is_empty() {
|
|
eprintln!("Failed to parse {} files:", failures.len());
|
|
for (file, reason) in &failures {
|
|
eprintln!(" - {:?}: {}", file, reason);
|
|
}
|
|
panic!("{} files failed to parse with tree-sitter", failures.len());
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Specific Language Feature Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_type_system_keywords_parse() {
|
|
let test_file = tree_sitter_dir()
|
|
.join("test")
|
|
.join("corpus")
|
|
.join("type_system.txt");
|
|
|
|
if !test_file.exists() {
|
|
panic!(
|
|
"Type system test corpus not found at {:?}. Please create it.",
|
|
test_file
|
|
);
|
|
}
|
|
|
|
let _test_file = tree_sitter_dir()
|
|
.join("test")
|
|
.join("corpus")
|
|
.join("type_system.txt");
|
|
|
|
let output = Command::new("tree-sitter")
|
|
.arg("test")
|
|
.arg("--file-name")
|
|
.arg("type_system.txt")
|
|
.current_dir(tree_sitter_dir())
|
|
.output()
|
|
.expect("Failed to run tree-sitter test");
|
|
|
|
if !output.status.success() {
|
|
eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(output.status.success(), "Type system test corpus failed");
|
|
}
|
|
|
|
#[test]
|
|
fn test_any_type_highlights_correctly() {
|
|
// Create a minimal test file with any_type usage
|
|
let test_content = r#"
|
|
concept_comparison SkillLevel {
|
|
Novice: { skill: any },
|
|
Expert: { skill: Tier is Expert }
|
|
}
|
|
"#;
|
|
|
|
let temp_dir = std::env::temp_dir();
|
|
let test_file = temp_dir.join("test_any_type.sb");
|
|
std::fs::write(&test_file, test_content).expect("Failed to write test file");
|
|
|
|
let output = Command::new("tree-sitter")
|
|
.arg("parse")
|
|
.arg(&test_file)
|
|
.current_dir(tree_sitter_dir())
|
|
.output()
|
|
.expect("Failed to run tree-sitter parse");
|
|
|
|
std::fs::remove_file(&test_file).ok(); // Clean up
|
|
|
|
if !output.status.success() {
|
|
eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(output.status.success(), "Failed to parse any_type usage");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("any_type"),
|
|
"Parse tree should contain any_type node"
|
|
);
|
|
assert!(
|
|
!stdout.contains("ERROR"),
|
|
"Parse tree should not contain ERROR nodes"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Zed Extension Build Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
#[ignore] // Only run with --ignored flag as this is slow
|
|
fn test_zed_extension_builds() {
|
|
let build_script = zed_extension_dir().join("build-extension.sh");
|
|
|
|
if !build_script.exists() {
|
|
panic!("build-extension.sh not found at {:?}", build_script);
|
|
}
|
|
|
|
let output = Command::new("bash")
|
|
.arg(&build_script)
|
|
.current_dir(zed_extension_dir())
|
|
.output()
|
|
.expect("Failed to run build-extension.sh");
|
|
|
|
if !output.status.success() {
|
|
eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(output.status.success(), "build-extension.sh failed");
|
|
|
|
// Verify the extension.wasm was created
|
|
let wasm = zed_extension_dir().join("extension.wasm");
|
|
assert!(wasm.exists(), "extension.wasm not created at {:?}", wasm);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extension_toml_has_valid_revision() {
|
|
let extension_toml = zed_extension_dir().join("extension.toml");
|
|
let content = std::fs::read_to_string(&extension_toml).expect("Failed to read extension.toml");
|
|
|
|
// Check that rev is set to a valid commit SHA
|
|
let rev_line = content
|
|
.lines()
|
|
.find(|line| line.starts_with("rev = "))
|
|
.expect("No 'rev' field found in extension.toml");
|
|
|
|
let rev = rev_line
|
|
.split('"')
|
|
.nth(1)
|
|
.expect("Could not parse rev value");
|
|
|
|
assert_eq!(
|
|
rev.len(),
|
|
40,
|
|
"Revision should be a 40-character git SHA, got: {}",
|
|
rev
|
|
);
|
|
|
|
// Verify the commit exists
|
|
let output = Command::new("git")
|
|
.arg("cat-file")
|
|
.arg("-t")
|
|
.arg(rev)
|
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
|
.output()
|
|
.expect("Failed to run git cat-file");
|
|
|
|
if !output.status.success() {
|
|
eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"Revision {} does not exist in git history",
|
|
rev
|
|
);
|
|
|
|
let obj_type = String::from_utf8_lossy(&output.stdout);
|
|
assert_eq!(
|
|
obj_type.trim(),
|
|
"commit",
|
|
"Revision {} is not a commit",
|
|
rev
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
fn find_sb_files(dir: &Path) -> Vec<PathBuf> {
|
|
let mut sb_files = Vec::new();
|
|
|
|
if !dir.exists() {
|
|
return sb_files;
|
|
}
|
|
|
|
visit_dirs(dir, &mut |path| {
|
|
if path.extension().and_then(|s| s.to_str()) == Some("sb") {
|
|
sb_files.push(path.to_path_buf());
|
|
}
|
|
});
|
|
|
|
sb_files
|
|
}
|
|
|
|
fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&Path)) {
|
|
if dir.is_dir() {
|
|
for entry in std::fs::read_dir(dir).unwrap() {
|
|
let entry = entry.unwrap();
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
visit_dirs(&path, cb);
|
|
} else {
|
|
cb(&path);
|
|
}
|
|
}
|
|
}
|
|
}
|