//! Shared deterministic fixtures used by translated compatibility tests. use std::collections::BTreeMap; use std::future::Future; use std::io; use std::path::{Component, Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Wake, Waker}; use std::time::Duration; /// Fails a not-yet-translated upstream test while retaining parity metadata. /// /// # Panics /// /// Always, with the retained upstream test identity. #[track_caller] pub fn pending( case_id: &str, source: &str, line: u64, test: &str, attribute: &str, body_sha256: &str, ) -> ! { panic!( "pending LibreMetaverse parity case {case_id}: {test} ({attribute}) from {source}:{line}; C# body sha256={body_sha256}" ) } /// Runs one test future without choosing an async runtime for the library. pub fn block_on(future: F) -> F::Output { struct ThreadWake(std::thread::Thread); impl Wake for ThreadWake { fn wake(self: Arc) { self.0.unpark(); } } let waker = Waker::from(Arc::new(ThreadWake(std::thread::current()))); let mut context = Context::from_waker(&waker); let mut future = std::pin::pin!(future); loop { match future.as_mut().poll(&mut context) { Poll::Ready(output) => return output, Poll::Pending => std::thread::park(), } } } /// Asserts equality within the exact absolute tolerance carried by an upstream test. /// /// # Panics /// /// Panics when the tolerance is invalid or the values differ beyond it. #[track_caller] pub fn assert_close(actual: f64, expected: f64, tolerance: f64) { assert!( tolerance.is_finite() && tolerance >= 0.0, "tolerance must be finite and non-negative" ); assert!( (actual - expected).abs() <= tolerance, "expected {expected:?} +/- {tolerance:?}, got {actual:?}" ); } /// Asserts byte equality and reports the first differing offset. /// /// # Panics /// /// Panics when the byte slices differ. #[track_caller] pub fn assert_bytes_eq(actual: &[u8], expected: &[u8]) { if actual == expected { return; } let offset = actual .iter() .zip(expected) .position(|(actual, expected)| actual != expected) .unwrap_or_else(|| actual.len().min(expected.len())); panic!( "byte fixtures differ at offset {offset}: expected {} bytes, got {} bytes", expected.len(), actual.len() ); } /// Decodes a whitespace-separated or contiguous hexadecimal byte fixture. /// /// # Errors /// /// Returns an error for an odd digit count or a non-hexadecimal digit. pub fn decode_hex(input: &str) -> Result, String> { let digits: Vec<_> = input .bytes() .filter(|byte| !byte.is_ascii_whitespace()) .collect(); if digits.len() % 2 != 0 { return Err("hex fixture has an odd number of digits".into()); } digits .chunks_exact(2) .enumerate() .map(|(index, pair)| { let high = hex_digit(pair[0]); let low = hex_digit(pair[1]); high.zip(low) .map(|(high, low)| high << 4 | low) .ok_or_else(|| format!("invalid hex byte at digit {}", index * 2)) }) .collect() } fn hex_digit(byte: u8) -> Option { match byte { b'0'..=b'9' => Some(byte - b'0'), b'a'..=b'f' => Some(byte - b'a' + 10), b'A'..=b'F' => Some(byte - b'A' + 10), _ => None, } } /// Resolves test data below a fixture root without permitting path traversal. /// /// # Errors /// /// Returns [`io::ErrorKind::InvalidInput`] for absolute or parent-relative paths. pub fn test_data_path(root: &Path, relative: &Path) -> io::Result { if relative.components().any(|component| { matches!( component, Component::ParentDir | Component::RootDir | Component::Prefix(_) ) }) { return Err(io::Error::new( io::ErrorKind::InvalidInput, "test data path must stay below its fixture root", )); } Ok(root.join(relative)) } /// Loads a test-data file through [`test_data_path`]. /// /// # Errors /// /// Returns path validation and file-read errors. pub fn load_test_data(root: &Path, relative: &Path) -> io::Result> { std::fs::read(test_data_path(root, relative)?) } /// A cloneable clock advanced only by the test. #[derive(Clone, Debug, Default)] pub struct ManualClock(Arc>); impl ManualClock { /// Returns the deterministic elapsed time. #[must_use] pub fn now(&self) -> Duration { *self .0 .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } /// Advances the clock and returns its new value. /// /// # Panics /// /// Panics if the resulting duration exceeds [`Duration::MAX`]. #[must_use] pub fn advance(&self, duration: Duration) -> Duration { let mut now = self .0 .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); *now += duration; *now } } /// A recorded deterministic HTTP-like request. #[derive(Clone, Debug, PartialEq, Eq)] pub struct RecordedRequest { pub method: String, pub uri: String, pub body: Vec, } /// A canned deterministic HTTP-like response. #[derive(Clone, Debug, PartialEq, Eq)] pub struct FakeResponse { pub status: u16, pub content_type: String, pub body: Vec, } /// An in-memory exact-URI/path fake with ordered request capture. #[derive(Debug, Default)] pub struct FakeNetwork { exact: Mutex>, paths: Mutex>, requests: Mutex>, } impl FakeNetwork { /// Adds an exact method/URI response. pub fn add_response(&self, method: &str, uri: &str, response: FakeResponse) { self.exact .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .insert((method.to_owned(), uri.to_owned()), response); } /// Adds a response matched after removing the request query string. pub fn add_path_response(&self, method: &str, uri_without_query: &str, response: FakeResponse) { self.paths .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .insert((method.to_owned(), uri_without_query.to_owned()), response); } /// Records a request and returns its configured response, or a deterministic 404. pub fn send(&self, method: &str, uri: &str, body: impl Into>) -> FakeResponse { self.requests .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(RecordedRequest { method: method.to_owned(), uri: uri.to_owned(), body: body.into(), }); let key = (method.to_owned(), uri.to_owned()); if let Some(response) = self .exact .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .get(&key) { return response.clone(); } let path_key = ( method.to_owned(), uri.split('?').next().unwrap_or(uri).to_owned(), ); self.paths .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .get(&path_key) .cloned() .unwrap_or_else(|| FakeResponse { status: 404, content_type: "application/octet-stream".into(), body: Vec::new(), }) } /// Returns all captured requests in send order. #[must_use] pub fn requests(&self) -> Vec { self.requests .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } } #[cfg(test)] mod tests { use super::*; #[test] fn deterministic_harness_covers_clock_network_and_bytes() { assert_eq!(block_on(std::future::ready(7)), 7); assert_close(1.001, 1.0, 0.01); assert_bytes_eq(&decode_hex("00 ff 2A").unwrap(), &[0, 255, 42]); assert!(decode_hex("é").is_err()); let clock = ManualClock::default(); assert_eq!( clock.advance(Duration::from_millis(25)), Duration::from_millis(25) ); assert_eq!(clock.now(), Duration::from_millis(25)); let network = FakeNetwork::default(); network.add_path_response( "GET", "https://example.test/cap", FakeResponse { status: 200, content_type: "application/llsd+xml".into(), body: b"fixture".to_vec(), }, ); assert_eq!( network.send("GET", "https://example.test/cap?tid=1", []), FakeResponse { status: 200, content_type: "application/llsd+xml".into(), body: b"fixture".to_vec(), } ); assert_eq!(network.requests()[0].uri, "https://example.test/cap?tid=1"); } #[test] fn test_data_rejects_parent_traversal() { assert_eq!( test_data_path(Path::new("fixtures"), Path::new("../secret")) .unwrap_err() .kind(), io::ErrorKind::InvalidInput ); } }