bevy_xray_parser 0.1.0

Parse Bevy source code to extract state hierarchies, plugin dependencies, and schedule configurations
Documentation
//! Integration tests for `parse_workspace` using temporary workspaces.

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use std::fs;

use bevy_xray_parser::graph;
use bevy_xray_parser::model::{StateGraph, StateKind};
use bevy_xray_parser::parser;

/// Create a minimal Bevy-like workspace in a temporary directory and parse it.
///
/// Uses a non-dot prefix for the temp directory because the parser filters out
/// paths containing `/.` (to skip hidden directories like `.git`).
fn create_and_parse(files: &[(&str, &str)]) -> StateGraph {
    let dir = tempfile::Builder::new()
        .prefix("bevy_xray_test_")
        .tempdir()
        .expect("failed to create temp dir");

    // Write a minimal Cargo.toml
    fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"test-crate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
    )
    .expect("failed to write Cargo.toml");

    // Write each source file
    let src = dir.path().join("src");
    fs::create_dir_all(&src).expect("failed to create src dir");
    for (name, content) in files {
        let path = src.join(name);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).expect("failed to create parent dir");
        }
        fs::write(&path, content).expect("failed to write source file");
    }

    parser::parse_workspace(dir.path()).expect("parse_workspace failed")
}

#[test]
fn parse_root_state() {
    let graph = create_and_parse(&[(
        "state.rs",
        r#"
            #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
            enum AppState {
                #[default]
                Loading,
                InGame,
                Menu,
            }
        "#,
    )]);

    assert_eq!(graph.states.len(), 1);
    assert_eq!(graph.states[0].name, "AppState");
    assert!(matches!(graph.states[0].kind, StateKind::Root));
    assert_eq!(graph.states[0].variants, vec!["Loading", "InGame", "Menu"]);
}

#[test]
fn parse_sub_state() {
    let graph = create_and_parse(&[(
        "state.rs",
        r#"
            #[derive(SubStates, Default, Debug, Clone, PartialEq, Eq, Hash)]
            #[source(AppState = InGame)]
            enum GameState {
                #[default]
                Playing,
                Paused,
            }
        "#,
    )]);

    assert_eq!(graph.states.len(), 1);
    assert_eq!(graph.states[0].name, "GameState");
    assert!(matches!(graph.states[0].kind, StateKind::Sub { .. }));
}

#[test]
fn parse_plugin_impl() {
    let graph = create_and_parse(&[(
        "plugin.rs",
        r#"
            struct MyPlugin;

            impl Plugin for MyPlugin {
                fn build(&self, app: &mut App) {
                    app.add_systems(Update, my_system.in_set(MySets::Core));
                }
            }
        "#,
    )]);

    assert_eq!(graph.plugins.len(), 1);
    assert_eq!(graph.plugins[0].name, "MyPlugin");
    assert_eq!(graph.plugins[0].crate_name, "test-crate");
}

#[test]
fn parse_plugin_with_sub_plugins() {
    let graph = create_and_parse(&[(
        "plugin.rs",
        r#"
            struct ParentPlugin;

            impl Plugin for ParentPlugin {
                fn build(&self, app: &mut App) {
                    app.add_plugins(ChildPlugin);
                }
            }

            struct ChildPlugin;

            impl Plugin for ChildPlugin {
                fn build(&self, app: &mut App) {}
            }
        "#,
    )]);

    assert_eq!(graph.plugins.len(), 2);
    let parent = graph
        .plugins
        .iter()
        .find(|p| p.name == "ParentPlugin")
        .expect("ParentPlugin");
    assert!(parent.sub_plugins.contains(&"ChildPlugin".to_string()));

    let tree = graph::build_plugin_tree(&graph);
    // Only ParentPlugin should be a root
    assert_eq!(tree.len(), 1);
    assert_eq!(tree[0].name, "ParentPlugin");
    assert_eq!(tree[0].children.len(), 1);
    assert_eq!(tree[0].children[0].name, "ChildPlugin");
}

#[test]
fn parse_system_set() {
    let graph = create_and_parse(&[(
        "sets.rs",
        r#"
            #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
            enum MySets {
                Core,
                Render,
            }
        "#,
    )]);

    assert_eq!(graph.system_sets.len(), 1);
    assert_eq!(graph.system_sets[0].name, "MySets");
    assert_eq!(graph.system_sets[0].variants, vec!["Core", "Render"]);
}

#[test]
fn parse_schedule_config() {
    let graph = create_and_parse(&[(
        "plugin.rs",
        r#"
            struct SetupPlugin;

            impl Plugin for SetupPlugin {
                fn build(&self, app: &mut App) {
                    app.configure_sets(
                        Update,
                        MySets::Core.run_if(in_state(AppState::InGame)),
                    );
                }
            }
        "#,
    )]);

    assert!(!graph.schedule_configs.is_empty());
    let config = &graph.schedule_configs[0];
    assert_eq!(config.schedule, "Update");
    assert!(config.system_sets.contains(&"MySets::Core".to_string()));
}

#[test]
fn parse_plugin_group() {
    let graph = create_and_parse(&[(
        "group.rs",
        r#"
            struct MyPlugins;

            impl PluginGroup for MyPlugins {
                fn build(self) -> PluginGroupBuilder {
                    PluginGroupBuilder::start::<Self>()
                        .add(FooPlugin)
                        .add(BarPlugin)
                }
            }
        "#,
    )]);

    assert_eq!(graph.plugin_groups.len(), 1);
    assert_eq!(graph.plugin_groups[0].name, "MyPlugins");
    assert!(
        graph.plugin_groups[0]
            .plugins
            .contains(&"FooPlugin".to_string())
    );
    assert!(
        graph.plugin_groups[0]
            .plugins
            .contains(&"BarPlugin".to_string())
    );
}

#[test]
fn parse_empty_workspace() {
    let graph = create_and_parse(&[("main.rs", "fn main() {}")]);

    assert!(graph.states.is_empty());
    assert!(graph.plugins.is_empty());
    assert!(graph.plugin_groups.is_empty());
    assert!(graph.schedule_configs.is_empty());
    assert!(graph.system_sets.is_empty());
}

#[test]
fn build_state_tree_integration() {
    let graph = create_and_parse(&[(
        "state.rs",
        r#"
            #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
            enum AppState {
                #[default]
                Loading,
                InGame,
            }

            #[derive(SubStates, Default, Debug, Clone, PartialEq, Eq, Hash)]
            #[source(AppState = InGame)]
            enum GameState {
                #[default]
                Playing,
                Paused,
            }
        "#,
    )]);

    let tree = graph::build_state_tree(&graph);
    assert_eq!(tree.len(), 1);
    assert_eq!(tree[0].name, "AppState");
    assert_eq!(tree[0].children.len(), 1);
    assert_eq!(tree[0].children[0].name, "GameState");
}

#[test]
fn plugins_for_state_integration() {
    let graph = create_and_parse(&[(
        "plugin.rs",
        r#"
            struct GamePlugin;

            impl Plugin for GamePlugin {
                fn build(&self, app: &mut App) {
                    app.init_state::<AppState>();
                    app.add_systems(Update, my_system.run_if(in_state(AppState::InGame)));
                }
            }
        "#,
    )]);

    let plugins = graph::plugins_for_state("AppState", &graph);
    assert_eq!(plugins.len(), 1);
    assert_eq!(plugins[0].name, "GamePlugin");
}