Add headless mode for automated testing
Implemented headless mode using MinimalPlugins and ScheduleRunnerPlugin to enable running the app without rendering, controlled via Unix socket. Changes: - Added conditional compilation based on 'headless' feature flag - Use MinimalPlugins with 60 FPS ScheduleRunner in headless mode - Skip rendering plugins (Camera, Rendering, DebugUI, SessionUI) - Made cube mesh/material assets optional for headless spawning - Direct NetworkingPlugin + PersistencePlugin instead of MarathonPlugin - Use app.run() instead of platform executor in headless mode This enables: - Running multiple instances for multi-client testing - Automated testing via marathonctl without GUI overhead - Background server instances for development - CI/CD integration for networking tests Refs #131, #132 Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
@@ -50,8 +50,8 @@ impl Plugin for CubePlugin {
|
|||||||
fn handle_spawn_cube(
|
fn handle_spawn_cube(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut messages: MessageReader<SpawnCubeEvent>,
|
mut messages: MessageReader<SpawnCubeEvent>,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: Option<ResMut<Assets<Mesh>>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: Option<ResMut<Assets<StandardMaterial>>>,
|
||||||
node_clock: Res<NodeVectorClock>,
|
node_clock: Res<NodeVectorClock>,
|
||||||
) {
|
) {
|
||||||
for event in messages.read() {
|
for event in messages.read() {
|
||||||
@@ -60,16 +60,8 @@ fn handle_spawn_cube(
|
|||||||
|
|
||||||
info!("Spawning cube {} at {:?}", entity_id, event.position);
|
info!("Spawning cube {} at {:?}", entity_id, event.position);
|
||||||
|
|
||||||
commands.spawn((
|
let mut entity = commands.spawn((
|
||||||
CubeMarker,
|
CubeMarker,
|
||||||
// Bevy 3D components
|
|
||||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
|
||||||
MeshMaterial3d(materials.add(StandardMaterial {
|
|
||||||
base_color: Color::srgb(0.8, 0.3, 0.6),
|
|
||||||
perceptual_roughness: 0.7,
|
|
||||||
metallic: 0.3,
|
|
||||||
..default()
|
|
||||||
})),
|
|
||||||
Transform::from_translation(event.position),
|
Transform::from_translation(event.position),
|
||||||
GlobalTransform::default(),
|
GlobalTransform::default(),
|
||||||
// Networking
|
// Networking
|
||||||
@@ -81,6 +73,19 @@ fn handle_spawn_cube(
|
|||||||
// Sync marker
|
// Sync marker
|
||||||
Synced,
|
Synced,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Only add rendering components if assets are available (non-headless mode)
|
||||||
|
if let (Some(ref mut meshes), Some(ref mut materials)) = (meshes.as_mut(), materials.as_mut()) {
|
||||||
|
entity.insert((
|
||||||
|
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||||
|
MeshMaterial3d(materials.add(StandardMaterial {
|
||||||
|
base_color: Color::srgb(0.8, 0.3, 0.6),
|
||||||
|
perceptual_roughness: 0.7,
|
||||||
|
metallic: 0.3,
|
||||||
|
..default()
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,13 @@ use libmarathon::{
|
|||||||
persistence::PersistenceConfig,
|
persistence::PersistenceConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "headless")]
|
||||||
|
use bevy::app::ScheduleRunnerPlugin;
|
||||||
|
#[cfg(feature = "headless")]
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
mod camera;
|
mod camera;
|
||||||
|
mod control;
|
||||||
mod cube;
|
mod cube;
|
||||||
mod debug_ui;
|
mod debug_ui;
|
||||||
mod engine_bridge;
|
mod engine_bridge;
|
||||||
@@ -115,46 +121,95 @@ fn main() {
|
|||||||
eprintln!(">>> Inserting EngineBridge resource");
|
eprintln!(">>> Inserting EngineBridge resource");
|
||||||
app.insert_resource(engine_bridge);
|
app.insert_resource(engine_bridge);
|
||||||
|
|
||||||
// Use DefaultPlugins but disable winit/window/input (we own those)
|
// Plugin setup based on headless vs rendering mode
|
||||||
eprintln!(">>> Adding DefaultPlugins");
|
#[cfg(not(feature = "headless"))]
|
||||||
app.add_plugins(
|
{
|
||||||
DefaultPlugins
|
info!("Adding DefaultPlugins (rendering mode)");
|
||||||
.build()
|
app.add_plugins(
|
||||||
.disable::<bevy::log::LogPlugin>() // Using tracing-subscriber
|
DefaultPlugins
|
||||||
.disable::<bevy::winit::WinitPlugin>() // We own winit
|
.build()
|
||||||
.disable::<bevy::window::WindowPlugin>() // We own the window
|
.disable::<bevy::log::LogPlugin>() // Using tracing-subscriber
|
||||||
.disable::<bevy::input::InputPlugin>() // We provide InputEvents directly
|
.disable::<bevy::winit::WinitPlugin>() // We own winit
|
||||||
.disable::<bevy::gilrs::GilrsPlugin>(), // We handle gamepad input ourselves
|
.disable::<bevy::window::WindowPlugin>() // We own the window
|
||||||
);
|
.disable::<bevy::input::InputPlugin>() // We provide InputEvents directly
|
||||||
eprintln!(">>> DefaultPlugins added");
|
.disable::<bevy::gilrs::GilrsPlugin>(), // We handle gamepad input ourselves
|
||||||
|
);
|
||||||
|
info!("DefaultPlugins added");
|
||||||
|
}
|
||||||
|
|
||||||
// Marathon core plugins (networking, debug UI, persistence)
|
#[cfg(feature = "headless")]
|
||||||
eprintln!(">>> Adding MarathonPlugin");
|
{
|
||||||
app.add_plugins(libmarathon::MarathonPlugin::new(
|
info!("Adding MinimalPlugins (headless mode)");
|
||||||
APP_NAME,
|
app.add_plugins(
|
||||||
PersistenceConfig {
|
MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(
|
||||||
flush_interval_secs: 2,
|
Duration::from_secs_f64(1.0 / 60.0), // 60 FPS
|
||||||
checkpoint_interval_secs: 30,
|
)),
|
||||||
battery_adaptive: true,
|
);
|
||||||
..Default::default()
|
info!("MinimalPlugins added");
|
||||||
},
|
}
|
||||||
));
|
|
||||||
eprintln!(">>> MarathonPlugin added");
|
// Marathon core plugins based on mode
|
||||||
|
#[cfg(not(feature = "headless"))]
|
||||||
|
{
|
||||||
|
info!("Adding MarathonPlugin (with debug UI)");
|
||||||
|
app.add_plugins(libmarathon::MarathonPlugin::new(
|
||||||
|
APP_NAME,
|
||||||
|
PersistenceConfig {
|
||||||
|
flush_interval_secs: 2,
|
||||||
|
checkpoint_interval_secs: 30,
|
||||||
|
battery_adaptive: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "headless")]
|
||||||
|
{
|
||||||
|
info!("Adding networking and persistence (headless, no UI)");
|
||||||
|
app.add_plugins(libmarathon::networking::NetworkingPlugin::new(Default::default()));
|
||||||
|
app.add_plugins(libmarathon::persistence::PersistencePlugin::with_config(
|
||||||
|
db_path.clone(),
|
||||||
|
PersistenceConfig {
|
||||||
|
flush_interval_secs: 2,
|
||||||
|
checkpoint_interval_secs: 30,
|
||||||
|
battery_adaptive: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Marathon plugins added");
|
||||||
|
|
||||||
// App-specific bridge for polling engine events
|
// App-specific bridge for polling engine events
|
||||||
eprintln!(">>> Adding app plugins");
|
info!("Adding app plugins");
|
||||||
app.add_plugins(EngineBridgePlugin);
|
app.add_plugins(EngineBridgePlugin);
|
||||||
app.add_plugins(CameraPlugin);
|
|
||||||
app.add_plugins(RenderingPlugin);
|
|
||||||
app.add_plugins(input::InputHandlerPlugin);
|
|
||||||
app.add_plugins(CubePlugin);
|
app.add_plugins(CubePlugin);
|
||||||
app.add_plugins(SelectionPlugin);
|
|
||||||
app.add_plugins(DebugUiPlugin);
|
|
||||||
app.add_plugins(SessionUiPlugin);
|
|
||||||
app.add_systems(Startup, initialize_offline_resources);
|
app.add_systems(Startup, initialize_offline_resources);
|
||||||
eprintln!(">>> All plugins added");
|
app.add_systems(Startup, control::start_control_socket_system);
|
||||||
|
|
||||||
eprintln!(">>> Running executor");
|
// Rendering-only plugins
|
||||||
libmarathon::platform::run_executor(app).expect("Failed to run executor");
|
#[cfg(not(feature = "headless"))]
|
||||||
eprintln!(">>> Executor returned (should never reach here)");
|
{
|
||||||
|
app.add_plugins(CameraPlugin);
|
||||||
|
app.add_plugins(RenderingPlugin);
|
||||||
|
app.add_plugins(input::InputHandlerPlugin);
|
||||||
|
app.add_plugins(SelectionPlugin);
|
||||||
|
app.add_plugins(DebugUiPlugin);
|
||||||
|
app.add_plugins(SessionUiPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("All plugins added");
|
||||||
|
|
||||||
|
// Run the app based on mode
|
||||||
|
#[cfg(not(feature = "headless"))]
|
||||||
|
{
|
||||||
|
info!("Running platform executor (rendering mode)");
|
||||||
|
libmarathon::platform::run_executor(app).expect("Failed to run executor");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "headless")]
|
||||||
|
{
|
||||||
|
info!("Running headless app loop");
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user