#[cfg(test)]
mod tests {
use crate::execution::list::TradeList;
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 {
sample_trade_with_maker(20, quantity)
}
fn sample_trade_with_maker(maker_id: u64, quantity: u64) -> Trade {
Trade::with_timestamp(
Id::from_uuid(parse_uuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8")),
Id::from_u64(10),
Id::from_u64(maker_id),
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:?}"),
}
}
}
#[test]
fn from_str_rejects_multibyte_without_panicking() {
let malformed = [
"MatchResult:order_id=é;remaining_quantity=60;is_complete=false;\
trades=Trades:[];filled_order_ids=[]",
"MatchResult:order_id=→;remaining_quantity=60;is_complete=false;\
trades=Trades:[];filled_order_ids=[]",
"MatchResult:order_id=10😀",
"MatchResult:é=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[];filled_order_ids=[]",
"MatchResult:order_id=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[é];filled_order_ids=[]",
"MatchResult:order_id=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[😀];filled_order_ids=[]",
"MatchResult:order_id=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[];filled_order_ids=[é]",
"MatchResult:order_id=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[];filled_order_ids=[1,→,2]",
"MatchResult:order_idé=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[];filled_order_ids=[]",
];
for input in malformed {
let parsed = MatchResult::from_str(input);
assert!(
parsed.is_err(),
"malformed multibyte input must parse to Err, got Ok for {input:?}"
);
}
}
#[test]
fn from_str_rejects_unterminated_multibyte_bracket() {
let inputs = [
"MatchResult:order_id=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[é",
"MatchResult:order_id=10;remaining_quantity=60;is_complete=false;\
trades=Trades:[];filled_order_ids=[😀",
];
for input in inputs {
assert!(
MatchResult::from_str(input).is_err(),
"unterminated multibyte bracket must parse to Err for {input:?}"
);
}
}
#[test]
fn display_round_trips_with_filled_ids_and_trades() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(result.add_trade(sample_trade_with_maker(20, 30)).is_ok());
assert!(result.add_trade(sample_trade_with_maker(21, 20)).is_ok());
result.add_filled_order_id(Id::from_u64(20));
result.add_filled_order_id(Id::from_u64(21));
let rendered = result.to_string();
let parsed = match MatchResult::from_str(&rendered) {
Ok(value) => value,
Err(error) => panic!("valid canonical output must round-trip: {error:?}"),
};
assert_eq!(parsed.order_id(), Id::from_u64(10));
assert_eq!(parsed.remaining_quantity().as_u64(), 50);
assert!(!parsed.is_complete());
assert_eq!(parsed.trades().len(), 2);
assert_eq!(
parsed.filled_order_ids(),
&[Id::from_u64(20), Id::from_u64(21)]
);
assert_eq!(parsed.to_string(), rendered);
}
#[test]
fn valid_result_with_filled_ids_round_trips_serde_json() {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(result.add_trade(sample_trade_with_maker(20, 40)).is_ok());
result.add_filled_order_id(Id::from_u64(20));
let json = serde_json::to_string(&result).expect("serialize");
let parsed: MatchResult = serde_json::from_str(&json).expect("valid payload must decode");
assert_eq!(parsed.order_id(), Id::from_u64(10));
assert_eq!(parsed.remaining_quantity().as_u64(), 60);
assert!(!parsed.is_complete());
assert_eq!(parsed.outcome(), MatchOutcome::PartiallyFilled);
assert_eq!(parsed.trades().len(), 1);
assert_eq!(parsed.filled_order_ids(), &[Id::from_u64(20)]);
}
#[test]
fn valid_killed_and_rejected_round_trip_serde_json() {
for build in [
MatchResult::mark_killed as fn(&mut MatchResult, u64),
MatchResult::mark_rejected,
] {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
build(&mut result, 100);
let json = serde_json::to_string(&result).expect("serialize");
let parsed: MatchResult =
serde_json::from_str(&json).expect("empty kill/reject must decode");
assert_eq!(parsed.remaining_quantity().as_u64(), 100);
assert!(!parsed.is_complete());
assert!(parsed.trades().is_empty());
assert!(parsed.filled_order_ids().is_empty());
assert!(parsed.was_killed() || parsed.was_rejected());
}
}
fn mutated_json(base: &MatchResult, mutate: impl FnOnce(&mut serde_json::Value)) -> String {
let mut value = serde_json::to_value(base).expect("serialize base");
mutate(&mut value);
serde_json::to_string(&value).expect("re-serialize mutated payload")
}
#[test]
fn deserialize_rejects_complete_with_remainder() {
let base = MatchResult::new(Id::from_u64(10), Quantity::new(0));
let json = mutated_json(&base, |v| {
v["remaining_quantity"] = serde_json::json!(5);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
}
#[test]
fn deserialize_rejects_trades_on_killed_or_rejected() {
let mut base = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(base.add_trade(sample_trade_with_maker(20, 40)).is_ok());
for outcome in ["killed", "rejected"] {
let json = mutated_json(&base, |v| {
v["outcome"] = serde_json::json!(outcome);
});
assert!(
serde_json::from_str::<MatchResult>(&json).is_err(),
"{outcome} with trades must be rejected"
);
}
}
#[test]
fn deserialize_rejects_filled_ids_on_killed() {
let mut base = MatchResult::new(Id::from_u64(10), Quantity::new(100));
base.mark_killed(100);
let json = mutated_json(&base, |v| {
v["filled_order_ids"] = serde_json::json!(["20"]);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
}
#[test]
fn deserialize_rejects_executed_quantity_sum_overflow() {
let mut base = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(base.add_trade(sample_trade_with_maker(20, 40)).is_ok());
assert!(base.add_trade(sample_trade_with_maker(21, 20)).is_ok());
let json = mutated_json(&base, |v| {
v["trades"]["trades"][0]["quantity"] = serde_json::json!(u64::MAX);
v["trades"]["trades"][1]["quantity"] = serde_json::json!(u64::MAX);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
}
#[test]
fn deserialize_rejects_executed_plus_remaining_overflow() {
let mut base = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(base.add_trade(sample_trade_with_maker(20, 40)).is_ok());
let json = mutated_json(&base, |v| {
v["trades"]["trades"][0]["quantity"] = serde_json::json!(u64::MAX);
v["remaining_quantity"] = serde_json::json!(5);
v["is_complete"] = serde_json::json!(false);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
}
#[test]
fn deserialize_rejects_filled_id_absent_from_trades() {
let mut base = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(base.add_trade(sample_trade_with_maker(20, 40)).is_ok());
base.add_filled_order_id(Id::from_u64(20));
let json = mutated_json(&base, |v| {
v["filled_order_ids"] = serde_json::json!(["20", "99"]);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
}
#[test]
fn from_str_rejects_complete_with_remainder() {
let text = "MatchResult:order_id=10;remaining_quantity=5;is_complete=true;\
trades=Trades:[];filled_order_ids=[]";
assert!(MatchResult::from_str(text).is_err());
}
#[test]
fn from_str_rejects_executed_quantity_sum_overflow() {
let trades = TradeList::from_vec(vec![
sample_trade_with_maker(20, u64::MAX),
sample_trade_with_maker(21, u64::MAX),
]);
let text = format!(
"MatchResult:order_id=10;remaining_quantity=1;is_complete=false;\
trades={trades};filled_order_ids=[]"
);
assert!(MatchResult::from_str(&text).is_err());
}
#[test]
fn from_str_rejects_filled_id_absent_from_trades() {
let trades = TradeList::from_vec(vec![sample_trade_with_maker(20, 40)]);
let text = format!(
"MatchResult:order_id=10;remaining_quantity=60;is_complete=false;\
trades={trades};filled_order_ids=[20,99]"
);
assert!(MatchResult::from_str(&text).is_err());
}
#[test]
fn from_str_rejects_trailing_content_after_filled_ids() {
let text = "MatchResult:order_id=10;remaining_quantity=0;is_complete=true;\
trades=Trades:[];filled_order_ids=[]order_id=10";
assert!(
MatchResult::from_str(text).is_err(),
"trailing content after filled_order_ids ']' must be rejected"
);
let ok = "MatchResult:order_id=10;remaining_quantity=0;is_complete=true;\
trades=Trades:[];filled_order_ids=[]";
assert!(MatchResult::from_str(ok).is_ok());
}
use proptest::prelude::*;
fn structural_fuzz() -> impl Strategy<Value = String> {
let token = prop_oneof![
Just("=".to_string()),
Just(";".to_string()),
Just("[".to_string()),
Just("]".to_string()),
Just("Trades:".to_string()),
Just("order_id".to_string()),
Just("remaining_quantity".to_string()),
Just("is_complete".to_string()),
Just("trades".to_string()),
Just("filled_order_ids".to_string()),
any::<char>().prop_map(|c| c.to_string()),
];
prop::collection::vec(token, 0..24)
.prop_map(|parts| format!("MatchResult:{}", parts.concat()))
}
proptest! {
#![proptest_config(ProptestConfig { cases: 1024, ..ProptestConfig::default() })]
#[test]
fn from_str_never_panics_on_arbitrary_utf8(suffix in any::<String>()) {
let with_prefix = format!("MatchResult:{suffix}");
let _ = MatchResult::from_str(&with_prefix);
let _ = MatchResult::from_str(&suffix);
}
#[test]
fn from_str_never_panics_on_structural_fuzz(input in structural_fuzz()) {
let _ = MatchResult::from_str(&input);
}
}
#[test]
fn deserialize_rejects_contradictory_explicit_outcome() {
let mut partial = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(partial.add_trade(sample_trade_with_maker(20, 40)).is_ok());
let empty = MatchResult::new(Id::from_u64(10), Quantity::new(100));
let complete = MatchResult::new(Id::from_u64(10), Quantity::new(0));
let json = mutated_json(&partial, |v| {
v["outcome"] = serde_json::json!("filled");
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
let json = mutated_json(&complete, |v| {
v["outcome"] = serde_json::json!("partially_filled");
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
let json = mutated_json(&empty, |v| {
v["outcome"] = serde_json::json!("partially_filled");
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
let json = mutated_json(&partial, |v| {
v["outcome"] = serde_json::json!("not_filled");
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
for outcome in ["killed", "rejected"] {
let json = mutated_json(&complete, |v| {
v["outcome"] = serde_json::json!(outcome);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
}
}
#[test]
fn deserialize_legacy_payload_without_outcome_derives() {
let mut partial = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(partial.add_trade(sample_trade_with_maker(20, 40)).is_ok());
let json = mutated_json(&partial, |v| {
if let Some(obj) = v.as_object_mut() {
obj.remove("outcome");
}
});
let decoded = match serde_json::from_str::<MatchResult>(&json) {
Ok(decoded) => decoded,
Err(err) => panic!("legacy payload must decode: {err}"),
};
assert_eq!(decoded.outcome(), MatchOutcome::PartiallyFilled);
}
#[test]
fn taker_identity_enforced_on_decode_and_add_trade() {
let mut base = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(base.add_trade(sample_trade_with_maker(20, 40)).is_ok());
let json = mutated_json(&base, |v| {
v["order_id"] = serde_json::json!(Id::from_u64(999));
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
let mut mismatched = MatchResult::new(Id::from_u64(999), Quantity::new(100));
assert!(
mismatched
.add_trade(sample_trade_with_maker(20, 40))
.is_err()
);
}
#[test]
fn deserialize_rejects_duplicate_or_out_of_order_filled_ids() {
let mut base = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(base.add_trade(sample_trade_with_maker(20, 40)).is_ok());
assert!(base.add_trade(sample_trade_with_maker(21, 60)).is_ok());
base.add_filled_order_id(Id::from_u64(20));
base.add_filled_order_id(Id::from_u64(21));
let json = mutated_json(&base, |v| {
v["filled_order_ids"] = serde_json::json!([Id::from_u64(20), Id::from_u64(20)]);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
let json = mutated_json(&base, |v| {
v["filled_order_ids"] = serde_json::json!([Id::from_u64(21), Id::from_u64(20)]);
});
assert!(serde_json::from_str::<MatchResult>(&json).is_err());
}
fn build_valid_result(initial: u64, specs: &[(u64, u64, bool)]) -> MatchResult {
let mut result = MatchResult::new(Id::from_u64(10), Quantity::new(initial));
let mut remaining = initial;
let mut filled = std::collections::HashSet::new();
for &(maker, raw_qty, fill) in specs {
if remaining == 0 {
break;
}
let qty = raw_qty % remaining + 1;
if result
.add_trade(sample_trade_with_maker(maker, qty))
.is_ok()
{
remaining -= qty;
if fill && filled.insert(maker) {
result.add_filled_order_id(Id::from_u64(maker));
}
}
}
result
}
#[test]
fn bincode_round_trip_is_symmetric_issue_135() {
let empty = MatchResult::new(Id::from_u64(10), Quantity::new(100));
let bytes = match bincode::serde::encode_to_vec(&empty, bincode::config::standard()) {
Ok(bytes) => bytes,
Err(error) => panic!("bincode encode failed: {error}"),
};
let decoded: MatchResult =
match bincode::serde::decode_from_slice(&bytes, bincode::config::standard()) {
Ok((value, _)) => value,
Err(error) => panic!("bincode decode failed: {error}"),
};
assert_eq!(decoded.outcome(), MatchOutcome::NotFilled);
assert_eq!(decoded.remaining_quantity().as_u64(), 100);
let mut partial = MatchResult::new(Id::from_u64(10), Quantity::new(100));
assert!(partial.add_trade(sample_trade(25)).is_ok());
let bytes = match bincode::serde::encode_to_vec(&partial, bincode::config::standard()) {
Ok(bytes) => bytes,
Err(error) => panic!("bincode encode failed: {error}"),
};
let decoded: MatchResult =
match bincode::serde::decode_from_slice(&bytes, bincode::config::standard()) {
Ok((value, _)) => value,
Err(error) => panic!("bincode decode failed: {error}"),
};
assert_eq!(decoded.outcome(), MatchOutcome::PartiallyFilled);
assert_eq!(decoded.remaining_quantity().as_u64(), 75);
assert_eq!(decoded.trades().len(), 1);
}
#[test]
fn json_payload_keeps_bare_outcome_issue_135() {
let result = MatchResult::new(Id::from_u64(10), Quantity::new(100));
let json = match serde_json::to_string(&result) {
Ok(json) => json,
Err(error) => panic!("json encode failed: {error}"),
};
assert!(
json.contains("\"outcome\":\"not_filled\""),
"outcome must stay a bare value in JSON, got: {json}"
);
}
proptest! {
#![proptest_config(ProptestConfig { cases: 256, ..ProptestConfig::default() })]
#[test]
fn valid_result_round_trips_both_encodings(
initial in 1u64..=100_000,
specs in prop::collection::vec((20u64..=40, 1u64..=100_000, any::<bool>()), 0..12),
) {
let original = build_valid_result(initial, &specs);
let bytes = bincode::serde::encode_to_vec(&original, bincode::config::standard())
.map_err(|e| TestCaseError::fail(format!("bincode encode: {e}")))?;
let (bin_decoded, _): (MatchResult, usize) =
bincode::serde::decode_from_slice(&bytes, bincode::config::standard())
.map_err(|e| TestCaseError::fail(format!("valid bincode must decode: {e}")))?;
prop_assert_eq!(bin_decoded.order_id(), original.order_id());
prop_assert_eq!(
bin_decoded.remaining_quantity().as_u64(),
original.remaining_quantity().as_u64()
);
prop_assert_eq!(bin_decoded.is_complete(), original.is_complete());
prop_assert_eq!(bin_decoded.outcome(), original.outcome());
prop_assert_eq!(bin_decoded.trades().len(), original.trades().len());
prop_assert_eq!(bin_decoded.filled_order_ids(), original.filled_order_ids());
let json = serde_json::to_string(&original)
.map_err(|e| TestCaseError::fail(format!("serialize: {e}")))?;
let decoded: MatchResult = serde_json::from_str(&json)
.map_err(|e| TestCaseError::fail(format!("valid json must decode: {e}")))?;
prop_assert_eq!(decoded.order_id(), original.order_id());
prop_assert_eq!(
decoded.remaining_quantity().as_u64(),
original.remaining_quantity().as_u64()
);
prop_assert_eq!(decoded.is_complete(), original.is_complete());
prop_assert_eq!(decoded.outcome(), original.outcome());
prop_assert_eq!(decoded.trades().len(), original.trades().len());
prop_assert_eq!(decoded.filled_order_ids(), original.filled_order_ids());
let text = original.to_string();
let reparsed = MatchResult::from_str(&text)
.map_err(|e| TestCaseError::fail(format!("valid text must reparse: {e}")))?;
prop_assert_eq!(
reparsed.remaining_quantity().as_u64(),
original.remaining_quantity().as_u64()
);
prop_assert_eq!(reparsed.is_complete(), original.is_complete());
prop_assert_eq!(reparsed.trades().len(), original.trades().len());
prop_assert_eq!(reparsed.filled_order_ids(), original.filled_order_ids());
}
}
}