forked from StrafesNET/strafe-project
move replay tests into test module
This commit is contained in:
parent
8d2ba28700
commit
affbada62e
@ -1912,7 +1912,6 @@ fn atomic_input_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:TimedI
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use crate::file;
|
||||
use crate::body::VirtualBody;
|
||||
use strafesnet_common::integer::{vec3::{self,int as int3},mat3};
|
||||
use super::*;
|
||||
@ -2127,202 +2126,4 @@ mod test{
|
||||
Time::ZERO
|
||||
),None);
|
||||
}
|
||||
#[test]
|
||||
fn run_replay(){
|
||||
println!("loading map file..");
|
||||
let map=file::load("../tools/bhop_maps/5692113331.snfm");
|
||||
println!("loading bot file..");
|
||||
let bot=file::load("../tools/replays/534s+997497968ns.snfb");
|
||||
if let (Ok(file::LoadFormat::Map(map)),Ok(file::LoadFormat::Bot(bot)))=(map,bot){
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
println!("simulating...");
|
||||
let mut physics=PhysicsState::default();
|
||||
for ins in bot.instructions{
|
||||
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
|
||||
}
|
||||
match physics.get_finish_time(){
|
||||
Some(time)=>println!("finish time:{}",time),
|
||||
None=>println!("simulation did not end in finished state"),
|
||||
}
|
||||
}else{
|
||||
panic!("missing files");
|
||||
}
|
||||
}
|
||||
enum DeterminismResult{
|
||||
Deterministic,
|
||||
NonDeterministic,
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum ReplayError{
|
||||
Load(file::LoadError),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
impl From<file::LoadError> for ReplayError{
|
||||
fn from(value:file::LoadError)->Self{
|
||||
Self::Load(value)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for ReplayError{
|
||||
fn from(value:std::io::Error)->Self{
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
|
||||
// create default physics state
|
||||
let mut physics_deterministic=PhysicsState::default();
|
||||
// create a second physics state
|
||||
let mut physics_filtered=PhysicsState::default();
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
println!("simulating...");
|
||||
|
||||
let mut non_idle_count=0;
|
||||
|
||||
for (i,ins) in bot.instructions.into_iter().enumerate(){
|
||||
let state_deterministic=physics_deterministic.clone();
|
||||
let state_filtered=physics_filtered.clone();
|
||||
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
|
||||
match ins{
|
||||
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
|
||||
other=>{
|
||||
non_idle_count+=1;
|
||||
// run
|
||||
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
|
||||
// check if position matches
|
||||
let b0=physics_deterministic.camera_body();
|
||||
let b1=physics_filtered.camera_body();
|
||||
if b0.position!=b1.position{
|
||||
println!("desync at instruction #{}",i);
|
||||
println!("non idle instructions completed={non_idle_count}");
|
||||
println!("instruction #{i}={:?}",other);
|
||||
println!("deterministic state0:\n{state_deterministic:?}");
|
||||
println!("filtered state0:\n{state_filtered:?}");
|
||||
println!("deterministic state1:\n{:?}",physics_deterministic);
|
||||
println!("filtered state1:\n{:?}",physics_filtered);
|
||||
return DeterminismResult::NonDeterministic;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
match physics_deterministic.get_finish_time(){
|
||||
Some(time)=>println!("[with idle] finish time:{}",time),
|
||||
None=>println!("[with idle] simulation did not end in finished state"),
|
||||
}
|
||||
match physics_filtered.get_finish_time(){
|
||||
Some(time)=>println!("[filtered] finish time:{}",time),
|
||||
None=>println!("[filtered] simulation did not end in finished state"),
|
||||
}
|
||||
DeterminismResult::Deterministic
|
||||
}
|
||||
type ThreadResult=Result<Option<DeterminismResult>,file::LoadError>;
|
||||
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
|
||||
s.spawn(move ||{
|
||||
let result=match file::load(file_path.as_path()){
|
||||
Ok(file::LoadFormat::Bot(bot))=>{
|
||||
println!("Running {:?}",file_path.file_stem());
|
||||
Ok(Some(segment_determinism(bot,physics_data)))
|
||||
},
|
||||
Ok(_)=>{
|
||||
println!("Provided bot file is not a bot file!");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e)=>{
|
||||
println!("Load error");
|
||||
Err(e)
|
||||
},
|
||||
};
|
||||
// send when thread is complete
|
||||
send.send(result).unwrap();
|
||||
});
|
||||
}
|
||||
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
|
||||
Ok(dir_entry.file_type()?.is_file().then_some(
|
||||
dir_entry.path()
|
||||
))
|
||||
}
|
||||
#[test]
|
||||
fn test_determinism()->Result<(),ReplayError>{
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
println!("loading map file..");
|
||||
let file::LoadFormat::Map(map)=file::load("../tools/bhop_maps/5692113331.snfm")? else{
|
||||
panic!("Provided map file is not a map file!");
|
||||
};
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let (send,recv)=std::sync::mpsc::channel();
|
||||
|
||||
let mut read_dir=std::fs::read_dir("../tools/replays")?;
|
||||
|
||||
// promise that &physics_data will outlive the spawned threads
|
||||
let thread_results=std::thread::scope(|s|{
|
||||
let mut thread_results=Vec::new();
|
||||
|
||||
// spawn threads
|
||||
println!("spawning up to {thread_limit} threads...");
|
||||
let mut active_thread_count=0;
|
||||
while active_thread_count<thread_limit{
|
||||
if let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
active_thread_count+=1;
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// spawn another thread every time a message is received from the channel
|
||||
println!("riding parallelism wave...");
|
||||
while let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
// wait for a thread to complete
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}
|
||||
|
||||
// wait for remaining threads to complete
|
||||
println!("waiting for all threads to complete...");
|
||||
for _ in 0..active_thread_count{
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
}
|
||||
|
||||
println!("done.");
|
||||
Ok::<_,ReplayError>(thread_results)
|
||||
})?;
|
||||
|
||||
// tally results
|
||||
#[derive(Default)]
|
||||
struct Totals{
|
||||
deterministic:u32,
|
||||
nondeterministic:u32,
|
||||
invalid:u32,
|
||||
error:u32,
|
||||
}
|
||||
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
|
||||
match result{
|
||||
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
|
||||
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
|
||||
Ok(None)=>totals.invalid+=1,
|
||||
Err(_)=>totals.error+=1,
|
||||
}
|
||||
totals
|
||||
});
|
||||
|
||||
println!("deterministic={deterministic}");
|
||||
println!("nondeterministic={nondeterministic}");
|
||||
println!("invalid={invalid}");
|
||||
println!("error={error}");
|
||||
|
||||
assert!(nondeterministic==0);
|
||||
assert!(invalid==0);
|
||||
assert!(error==0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,9 @@ mod compat_worker;
|
||||
mod physics_worker;
|
||||
mod graphics_worker;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));
|
||||
|
||||
fn main(){
|
||||
|
1
strafe-client/src/tests/mod.rs
Normal file
1
strafe-client/src/tests/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
mod replay;
|
203
strafe-client/src/tests/replay.rs
Normal file
203
strafe-client/src/tests/replay.rs
Normal file
@ -0,0 +1,203 @@
|
||||
|
||||
use crate::file;
|
||||
|
||||
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
|
||||
|
||||
#[test]
|
||||
fn run_replay(){
|
||||
println!("loading map file..");
|
||||
let map=file::load("../tools/bhop_maps/5692113331.snfm");
|
||||
println!("loading bot file..");
|
||||
let bot=file::load("../tools/replays/534s+997497968ns.snfb");
|
||||
if let (Ok(file::LoadFormat::Map(map)),Ok(file::LoadFormat::Bot(bot)))=(map,bot){
|
||||
// create recording
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
println!("simulating...");
|
||||
let mut physics=PhysicsState::default();
|
||||
for ins in bot.instructions{
|
||||
PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
|
||||
}
|
||||
match physics.get_finish_time(){
|
||||
Some(time)=>println!("finish time:{}",time),
|
||||
None=>println!("simulation did not end in finished state"),
|
||||
}
|
||||
}else{
|
||||
panic!("missing files");
|
||||
}
|
||||
}
|
||||
enum DeterminismResult{
|
||||
Deterministic,
|
||||
NonDeterministic,
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
enum ReplayError{
|
||||
Load(file::LoadError),
|
||||
IO(std::io::Error),
|
||||
}
|
||||
impl From<file::LoadError> for ReplayError{
|
||||
fn from(value:file::LoadError)->Self{
|
||||
Self::Load(value)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for ReplayError{
|
||||
fn from(value:std::io::Error)->Self{
|
||||
Self::IO(value)
|
||||
}
|
||||
}
|
||||
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->DeterminismResult{
|
||||
// create default physics state
|
||||
let mut physics_deterministic=PhysicsState::default();
|
||||
// create a second physics state
|
||||
let mut physics_filtered=PhysicsState::default();
|
||||
|
||||
// invent a new bot id and insert the replay
|
||||
println!("simulating...");
|
||||
|
||||
let mut non_idle_count=0;
|
||||
|
||||
for (i,ins) in bot.instructions.into_iter().enumerate(){
|
||||
let state_deterministic=physics_deterministic.clone();
|
||||
let state_filtered=physics_filtered.clone();
|
||||
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
|
||||
match ins{
|
||||
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
|
||||
other=>{
|
||||
non_idle_count+=1;
|
||||
// run
|
||||
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
|
||||
// check if position matches
|
||||
let b0=physics_deterministic.camera_body();
|
||||
let b1=physics_filtered.camera_body();
|
||||
if b0.position!=b1.position{
|
||||
println!("desync at instruction #{}",i);
|
||||
println!("non idle instructions completed={non_idle_count}");
|
||||
println!("instruction #{i}={:?}",other);
|
||||
println!("deterministic state0:\n{state_deterministic:?}");
|
||||
println!("filtered state0:\n{state_filtered:?}");
|
||||
println!("deterministic state1:\n{:?}",physics_deterministic);
|
||||
println!("filtered state1:\n{:?}",physics_filtered);
|
||||
return DeterminismResult::NonDeterministic;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
match physics_deterministic.get_finish_time(){
|
||||
Some(time)=>println!("[with idle] finish time:{}",time),
|
||||
None=>println!("[with idle] simulation did not end in finished state"),
|
||||
}
|
||||
match physics_filtered.get_finish_time(){
|
||||
Some(time)=>println!("[filtered] finish time:{}",time),
|
||||
None=>println!("[filtered] simulation did not end in finished state"),
|
||||
}
|
||||
DeterminismResult::Deterministic
|
||||
}
|
||||
type ThreadResult=Result<Option<DeterminismResult>,file::LoadError>;
|
||||
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
|
||||
s.spawn(move ||{
|
||||
let result=match file::load(file_path.as_path()){
|
||||
Ok(file::LoadFormat::Bot(bot))=>{
|
||||
println!("Running {:?}",file_path.file_stem());
|
||||
Ok(Some(segment_determinism(bot,physics_data)))
|
||||
},
|
||||
Ok(_)=>{
|
||||
println!("Provided bot file is not a bot file!");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e)=>{
|
||||
println!("Load error");
|
||||
Err(e)
|
||||
},
|
||||
};
|
||||
// send when thread is complete
|
||||
send.send(result).unwrap();
|
||||
});
|
||||
}
|
||||
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
|
||||
Ok(dir_entry.file_type()?.is_file().then_some(
|
||||
dir_entry.path()
|
||||
))
|
||||
}
|
||||
#[test]
|
||||
fn test_determinism()->Result<(),ReplayError>{
|
||||
let thread_limit=std::thread::available_parallelism()?.get();
|
||||
println!("loading map file..");
|
||||
let file::LoadFormat::Map(map)=file::load("../tools/bhop_maps/5692113331.snfm")? else{
|
||||
panic!("Provided map file is not a map file!");
|
||||
};
|
||||
let mut physics_data=PhysicsData::default();
|
||||
println!("generating models..");
|
||||
physics_data.generate_models(&map);
|
||||
let (send,recv)=std::sync::mpsc::channel();
|
||||
|
||||
let mut read_dir=std::fs::read_dir("../tools/replays")?;
|
||||
|
||||
// promise that &physics_data will outlive the spawned threads
|
||||
let thread_results=std::thread::scope(|s|{
|
||||
let mut thread_results=Vec::new();
|
||||
|
||||
// spawn threads
|
||||
println!("spawning up to {thread_limit} threads...");
|
||||
let mut active_thread_count=0;
|
||||
while active_thread_count<thread_limit{
|
||||
if let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
active_thread_count+=1;
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// spawn another thread every time a message is received from the channel
|
||||
println!("riding parallelism wave...");
|
||||
while let Some(dir_entry_result)=read_dir.next(){
|
||||
if let Some(file_path)=get_file_path(dir_entry_result?)?{
|
||||
// wait for a thread to complete
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
do_thread(s,file_path,send.clone(),&physics_data);
|
||||
}
|
||||
}
|
||||
|
||||
// wait for remaining threads to complete
|
||||
println!("waiting for all threads to complete...");
|
||||
for _ in 0..active_thread_count{
|
||||
thread_results.push(recv.recv().unwrap());
|
||||
}
|
||||
|
||||
println!("done.");
|
||||
Ok::<_,ReplayError>(thread_results)
|
||||
})?;
|
||||
|
||||
// tally results
|
||||
#[derive(Default)]
|
||||
struct Totals{
|
||||
deterministic:u32,
|
||||
nondeterministic:u32,
|
||||
invalid:u32,
|
||||
error:u32,
|
||||
}
|
||||
let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
|
||||
match result{
|
||||
Ok(Some(DeterminismResult::Deterministic))=>totals.deterministic+=1,
|
||||
Ok(Some(DeterminismResult::NonDeterministic))=>totals.nondeterministic+=1,
|
||||
Ok(None)=>totals.invalid+=1,
|
||||
Err(_)=>totals.error+=1,
|
||||
}
|
||||
totals
|
||||
});
|
||||
|
||||
println!("deterministic={deterministic}");
|
||||
println!("nondeterministic={nondeterministic}");
|
||||
println!("invalid={invalid}");
|
||||
println!("error={error}");
|
||||
|
||||
assert!(nondeterministic==0);
|
||||
assert!(invalid==0);
|
||||
assert!(error==0);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user