#[cfg(test)]
mod tests {
use crate::execution::match_result::{MatchOutcome, MatchResult};
use crate::execution::trade::Trade;
use crate::orders::{Id, Side};
use crate::utils::{Price, Quantity, TimestampMs};
use std::str::FromStr;
use uuid::Uuid;
fn parse_uuid(input: &str) -> Uuid {
match Uuid::parse_str(input) {
Ok(value) => value,
Err(error) => panic!("failed to parse uuid: {error}"),
}
}
fn sample_trade(quantity: u64) -> Trade {
Trade::with_timestamp(
Id::from_uuid(parse_uuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8")),
Id::from_u64(10),
Id::from_u64(20),
Price::new(1_000),
Quantity::new(quantity),
Side::Buy,
TimestampMs::new(1_616_823_000_000),
)
}
#[test]
fn add_trade_updates_remaining_and_trades() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(result.add_trade(sample_trade(25)).is_ok());
assert_eq!(result.remaining_quantity().as_u64(), 75);
assert_eq!(result.trades().len(), 1);
assert!(!result.is_complete());
}
#[test]
fn display_and_parse_use_trades_field() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(result.add_trade(sample_trade(40)).is_ok());
let rendered = result.to_string();
assert!(rendered.contains(";trades=Trades:[Trade:"));
let parsed = match MatchResult::from_str(&rendered) {
Ok(value) => value,
Err(error) => panic!("failed to parse match result: {error:?}"),
};
assert_eq!(parsed.trades().len(), 1);
assert_eq!(parsed.remaining_quantity().as_u64(), 60);
}
#[test]
fn add_trade_keeps_outcome_in_sync() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert_eq!(result.outcome(), MatchOutcome::NotFilled);
assert!(result.add_trade(sample_trade(40)).is_ok());
assert_eq!(result.outcome(), MatchOutcome::PartiallyFilled);
assert!(!result.was_killed());
assert!(!result.was_rejected());
assert!(result.add_trade(sample_trade(60)).is_ok());
assert_eq!(result.outcome(), MatchOutcome::Filled);
assert!(result.is_complete());
}
#[test]
fn outcome_survives_serde_json_roundtrip() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(result.add_trade(sample_trade(40)).is_ok());
let json = serde_json::to_string(&result).expect("serialize match result");
let parsed: MatchResult = serde_json::from_str(&json).expect("deserialize match result");
assert_eq!(parsed.outcome(), MatchOutcome::PartiallyFilled);
assert_eq!(parsed.remaining_quantity().as_u64(), 60);
assert_eq!(parsed.trades().len(), 1);
}
#[test]
fn outcome_defaults_when_absent_from_json() {
let result = MatchResult::new(Id::from_u64(10), Quantity::new(70));
let mut value: serde_json::Value =
serde_json::to_value(&result).expect("serialize match result");
value
.as_object_mut()
.expect("object")
.remove("outcome")
.expect("current payload carries an outcome field");
let legacy = serde_json::to_string(&value).expect("re-serialize legacy payload");
let parsed: MatchResult =
serde_json::from_str(&legacy).expect("legacy match result must deserialize");
assert_eq!(parsed.outcome(), MatchOutcome::NotFilled);
assert_eq!(parsed.remaining_quantity().as_u64(), 70);
}
#[test]
fn from_str_rejects_old_transactions_field() {
let old_payload = "MatchResult:order_id=1;remaining_quantity=1;is_complete=false;transactions=Transactions:[];filled_order_ids=[]";
let parsed = MatchResult::from_str(old_payload);
assert!(parsed.is_err());
}
#[test]
fn add_trade_rejects_underflow() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(10));
let error = result.add_trade(sample_trade(11));
assert!(error.is_err());
assert_eq!(result.remaining_quantity().as_u64(), 10);
assert_eq!(result.trades().len(), 0);
}
#[test]
fn executed_value_rejects_overflow() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(4));
let trade = Trade::with_timestamp(
Id::from_uuid(parse_uuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8")),
Id::from_u64(10),
Id::from_u64(20),
Price::new(u128::MAX),
Quantity::new(2),
Side::Buy,
TimestampMs::new(1_616_823_000_000),
);
assert!(result.add_trade(trade).is_ok());
assert!(result.executed_value().is_err());
}
#[test]
fn test_average_price_zero_executed_quantity_returns_none() {
let result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
match result.average_price() {
Ok(None) => {}
other => panic!("expected Ok(None) for zero executed quantity, got {other:?}"),
}
}
#[test]
fn test_average_price_exact_small_values_is_precise() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(result.add_trade(sample_trade(30)).is_ok());
match result.average_price() {
Ok(Some(avg)) => {
assert!(avg.is_finite(), "average price must be finite");
assert!((avg - 1000.0_f64).abs() < f64::EPSILON);
}
other => panic!("expected Ok(Some(1000.0)), got {other:?}"),
}
}
#[test]
fn test_average_price_large_values_loses_f64_precision_but_stays_finite() {
const PRICE: u128 = (1_u128 << 53) + 1; const EXACT_INT_AVG: u128 = PRICE;
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(1));
let trade = Trade::with_timestamp(
Id::from_uuid(parse_uuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8")),
Id::from_u64(10),
Id::from_u64(20),
Price::new(PRICE),
Quantity::new(1),
Side::Buy,
TimestampMs::new(1_616_823_000_000),
);
assert!(result.add_trade(trade).is_ok());
match result.average_price() {
Ok(Some(avg)) => {
assert!(avg.is_finite(), "average price must be finite");
assert!(!avg.is_nan(), "average price must not be NaN");
assert!(!avg.is_infinite(), "average price must not be Inf");
assert_eq!(
avg, 9_007_199_254_740_992.0_f64,
"observed f64 average rounds 2^53 + 1 down to 2^53"
);
let avg_as_int = avg as u128;
assert_eq!(
EXACT_INT_AVG - avg_as_int,
1,
"f64 average loses exactly 1 vs the exact integer average (2^53 + 1)"
);
}
other => panic!("expected Ok(Some(_)), got {other:?}"),
}
}
#[test]
fn test_average_price_valid_inputs_never_nan_or_inf() {
let cases: [(u128, u64); 4] = [
(1, 1),
(1_000, 7),
(u64::MAX as u128, 3),
((1_u128 << 60) + 5, 11),
];
for (idx, (price, quantity)) in cases.into_iter().enumerate() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(quantity));
let trade = Trade::with_timestamp(
Id::from_uuid(parse_uuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8")),
Id::from_u64(10),
Id::from_u64(20),
Price::new(price),
Quantity::new(quantity),
Side::Buy,
TimestampMs::new(1_616_823_000_000),
);
assert!(result.add_trade(trade).is_ok());
match result.average_price() {
Ok(Some(avg)) => {
assert!(
avg.is_finite(),
"case {idx}: average must be finite (price={price}, qty={quantity})"
);
}
other => panic!("case {idx}: expected Ok(Some(_)), got {other:?}"),
}
}
}
}