Build strict NUnit parity harness
Some checks failed
Rust API gates / api-gates (push) Has been cancelled

This commit is contained in:
2026-08-08 16:47:41 +02:00
parent c8a953996e
commit 80fbe87b98
10 changed files with 19784 additions and 17083 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,275 @@
//! Shared failure marker used by generated compatibility tests.
//! Shared deterministic fixtures used by translated compatibility tests.
/// Fails a not-yet-translated upstream test while retaining parity metadata.
use std::collections::BTreeMap;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
/// Asserts equality within the exact absolute tolerance carried by an upstream test.
///
/// # Panics
///
/// Always, with the retained upstream test identity.
/// Panics when the tolerance is invalid or the values differ beyond it.
#[track_caller]
pub fn pending(source: &str, line: u64, test: &str, attribute: &str, body_sha256: &str) -> ! {
panic!(
"pending LibreMetaverse parity test {test} ({attribute}) from {source}:{line}; C# body sha256={body_sha256}"
)
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<Vec<u8>, 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<u8> {
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<PathBuf> {
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<Vec<u8>> {
std::fs::read(test_data_path(root, relative)?)
}
/// A cloneable clock advanced only by the test.
#[derive(Clone, Debug, Default)]
pub struct ManualClock(Arc<Mutex<Duration>>);
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<u8>,
}
/// A canned deterministic HTTP-like response.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FakeResponse {
pub status: u16,
pub content_type: String,
pub body: Vec<u8>,
}
/// An in-memory exact-URI/path fake with ordered request capture.
#[derive(Debug, Default)]
pub struct FakeNetwork {
exact: Mutex<BTreeMap<(String, String), FakeResponse>>,
paths: Mutex<BTreeMap<(String, String), FakeResponse>>,
requests: Mutex<Vec<RecordedRequest>>,
}
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<Vec<u8>>) -> 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<RecordedRequest> {
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_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
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff