From affbada62e90496a44e623e67ab971878730f158 Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Wed, 22 Jan 2025 05:36:38 -0800
Subject: [PATCH] move replay tests into test module

---
 engine/physics/src/physics.rs     | 199 -----------------------------
 strafe-client/src/main.rs         |   3 +
 strafe-client/src/tests/mod.rs    |   1 +
 strafe-client/src/tests/replay.rs | 203 ++++++++++++++++++++++++++++++
 4 files changed, 207 insertions(+), 199 deletions(-)
 create mode 100644 strafe-client/src/tests/mod.rs
 create mode 100644 strafe-client/src/tests/replay.rs

diff --git a/engine/physics/src/physics.rs b/engine/physics/src/physics.rs
index 709efb79..19ecc8f3 100644
--- a/engine/physics/src/physics.rs
+++ b/engine/physics/src/physics.rs
@@ -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(())
-	}
 }
diff --git a/strafe-client/src/main.rs b/strafe-client/src/main.rs
index 65beb0f0..efd75d8c 100644
--- a/strafe-client/src/main.rs
+++ b/strafe-client/src/main.rs
@@ -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(){
diff --git a/strafe-client/src/tests/mod.rs b/strafe-client/src/tests/mod.rs
new file mode 100644
index 00000000..6160572f
--- /dev/null
+++ b/strafe-client/src/tests/mod.rs
@@ -0,0 +1 @@
+mod replay;
diff --git a/strafe-client/src/tests/replay.rs b/strafe-client/src/tests/replay.rs
new file mode 100644
index 00000000..9d25d3b9
--- /dev/null
+++ b/strafe-client/src/tests/replay.rs
@@ -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(())
+}