Skip to main content

scene_corpus/
scene_corpus.rs

1//! Ad-hoc corpus runner for the M0 `.tscn`/`.tres` parser — parses every scene/resource under a
2//! directory and reports panics + a problem summary (the robustness gate, Playbook §8.2).
3//!
4//! Usage: `cargo run -p gdscript-scene --example scene_corpus -- <dir> [--show]`
5
6use std::path::{Path, PathBuf};
7
8use gdscript_scene::parse_scene;
9
10fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
11    let Ok(entries) = std::fs::read_dir(dir) else {
12        return;
13    };
14    for entry in entries.flatten() {
15        let path = entry.path();
16        if path.is_dir() {
17            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
18            if matches!(name, ".godot" | ".git" | "target" | "node_modules") {
19                continue;
20            }
21            collect(&path, out);
22        } else if matches!(
23            path.extension().and_then(|e| e.to_str()),
24            Some("tscn" | "tres")
25        ) {
26            out.push(path);
27        }
28    }
29}
30
31fn main() {
32    let args: Vec<String> = std::env::args().skip(1).collect();
33    let dir = args
34        .first()
35        .cloned()
36        .expect("usage: scene_corpus <dir> [--show]");
37    let show = args.iter().any(|a| a == "--show");
38    // `--ci`: exit non-zero on any panic (the scene parser never errors — only `problems` — so a
39    // panic is the only hard failure for the robustness gate).
40    let ci = args.iter().any(|a| a == "--ci");
41
42    let mut files = Vec::new();
43    collect(Path::new(&dir), &mut files);
44    files.sort();
45
46    let (mut clean, mut with_problems, mut total_problems, mut nodes_total) = (0usize, 0, 0, 0);
47    let mut panics = Vec::new();
48    for path in &files {
49        let Ok(src) = std::fs::read_to_string(path) else {
50            continue; // non-UTF-8 / unreadable — not a text scene
51        };
52        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse_scene(&src))) {
53            Ok(m) => {
54                nodes_total += m.nodes.len();
55                if m.problems.is_empty() {
56                    clean += 1;
57                } else {
58                    with_problems += 1;
59                    total_problems += m.problems.len();
60                    if show {
61                        println!("\n{}  ({} problems)", path.display(), m.problems.len());
62                        for p in &m.problems {
63                            println!("  {p:?}");
64                        }
65                    }
66                }
67            }
68            Err(_) => panics.push(path.clone()),
69        }
70    }
71
72    println!(
73        "\n=== scene corpus: {dir} ===\n  files:        {}\n  clean:        {clean}\n  with problems: {with_problems} ({total_problems} problems)\n  nodes parsed:  {nodes_total}\n  panics:        {}",
74        files.len(),
75        panics.len()
76    );
77    for p in &panics {
78        println!("  PANIC: {}", p.display());
79    }
80    if ci && !panics.is_empty() {
81        eprintln!("SCENE CORPUS GATE FAILED: {} panics", panics.len());
82        std::process::exit(1);
83    }
84}