pub mod static_test_game;
pub mod animated_test_game;
pub use static_test_game::run_static_test_game;
pub use animated_test_game::run_animated_test_game;
use std::process::{Child, Command};
use std::time::Duration;
use tokio::time::timeout;
#[derive(Debug, Clone)]
pub struct TestGameConfig {
pub brp_port: u16,
pub width: u32,
pub height: u32,
pub startup_timeout: Duration,
pub headless: bool,
}
impl Default for TestGameConfig {
fn default() -> Self {
Self {
brp_port: 15702,
width: 800,
height: 600,
startup_timeout: Duration::from_secs(10),
headless: std::env::var("CI").is_ok(), }
}
}
pub struct TestGameLauncher {
config: TestGameConfig,
}
impl TestGameLauncher {
pub fn new(config: TestGameConfig) -> Self {
Self { config }
}
pub async fn launch_static_game(&self) -> Result<TestGameInstance, Box<dyn std::error::Error>> {
self.launch_game_binary("static_test_game").await
}
pub async fn launch_animated_game(&self) -> Result<TestGameInstance, Box<dyn std::error::Error>> {
self.launch_game_binary("animated_test_game").await
}
async fn launch_game_binary(&self, game_type: &str) -> Result<TestGameInstance, Box<dyn std::error::Error>> {
let mut cmd = Command::new("cargo");
cmd.args(&["run", "--bin", &format!("test_{}", game_type)])
.env("BEVY_BRP_PORT", self.config.brp_port.to_string())
.env("BEVY_WINDOW_WIDTH", self.config.width.to_string())
.env("BEVY_WINDOW_HEIGHT", self.config.height.to_string());
if self.config.headless {
cmd.env("BEVY_HEADLESS", "1");
}
let child = cmd.spawn()?;
let startup_result = timeout(
self.config.startup_timeout,
self.wait_for_brp_connection()
).await;
match startup_result {
Ok(Ok(())) => {
Ok(TestGameInstance {
process: Some(child),
brp_port: self.config.brp_port,
config: self.config.clone(),
})
}
Ok(Err(e)) => {
if let Ok(mut child) = Command::new("pkill")
.arg("-f")
.arg(&format!("test_{}", game_type))
.spawn()
{
let _ = child.wait();
}
Err(e)
}
Err(_) => {
Err("Game startup timeout".into())
}
}
}
async fn wait_for_brp_connection(&self) -> Result<(), Box<dyn std::error::Error>> {
use tokio::net::TcpStream;
use tokio::time::{sleep, Duration};
let max_attempts = 50;
let retry_delay = Duration::from_millis(200);
for attempt in 1..=max_attempts {
match TcpStream::connect(format!("127.0.0.1:{}", self.config.brp_port)).await {
Ok(_) => {
println!("BRP connection established on attempt {}", attempt);
return Ok(());
}
Err(_) => {
if attempt < max_attempts {
sleep(retry_delay).await;
}
}
}
}
Err("Failed to establish BRP connection".into())
}
}
pub struct TestGameInstance {
process: Option<Child>,
pub brp_port: u16,
pub config: TestGameConfig,
}
impl TestGameInstance {
pub fn brp_url(&self) -> String {
format!("http://127.0.0.1:{}", self.brp_port)
}
pub fn shutdown(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(mut child) = self.process.take() {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
let pid = Pid::from_raw(child.id() as i32);
if let Err(_) = signal::kill(pid, Signal::SIGTERM) {
let _ = child.kill();
}
}
#[cfg(not(unix))]
{
let _ = child.kill();
}
let _ = child.wait();
}
Ok(())
}
}
impl Drop for TestGameInstance {
fn drop(&mut self) {
let _ = self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_game_launcher_config() {
let config = TestGameConfig::default();
assert_eq!(config.brp_port, 15702);
assert_eq!(config.width, 800);
assert_eq!(config.height, 600);
}
#[test]
fn test_game_instance_url() {
let config = TestGameConfig::default();
let instance = TestGameInstance {
process: None,
brp_port: 15702,
config,
};
assert_eq!(instance.brp_url(), "http://127.0.0.1:15702");
}
}