use melin_journal::codec;
use melin_server::exchange_app::ServerApp;
use melin_trading::trading_event::TradingEvent;
#[test]
fn fuzz_journal_decode() {
bolero::check!().for_each(|data: &[u8]| {
let _ = codec::decode::<TradingEvent>(data);
});
}
#[test]
fn fuzz_journal_file_header() {
bolero::check!().for_each(|data: &[u8]| {
let _ = codec::decode_file_header(data);
});
}
#[test]
fn fuzz_journal_roundtrip() {
bolero::check!().for_each(|data: &[u8]| {
let Some(event) = journal_event_from_bytes(data) else {
return;
};
let seq = 1u64;
let ts = 1_700_000_000_000_000_000u64;
let mut buf = [0u8; 272];
let written = match codec::encode(seq, ts, 0, 0, &event, &mut buf) {
Ok(n) => n,
Err(_) => return,
};
let (consumed, dec_seq, dec_ts, _kh, _rs, dec_event) =
codec::decode(&buf[..written]).expect("decode of freshly encoded event must succeed");
assert_eq!(consumed, written);
assert_eq!(dec_seq, seq);
assert_eq!(dec_ts, ts);
assert_eq!(dec_event, event, "round-trip mismatch");
});
}
#[test]
fn fuzz_snapshot_decode() {
bolero::check!().for_each(|data: &[u8]| {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("fuzz.snapshot");
std::fs::write(&path, data).expect("write");
let _ = melin_transport_core::snapshot::load::<ServerApp>(&path);
});
}
use melin_journal::JournalEvent;
use melin_types::types::*;
use std::num::NonZeroU64;
fn nz64(data: &[u8], offset: usize) -> Option<NonZeroU64> {
if data.len() < offset + 8 {
return None;
}
NonZeroU64::new(u64::from_le_bytes(
data[offset..offset + 8].try_into().ok()?,
))
}
fn u32_at(data: &[u8], offset: usize) -> Option<u32> {
if data.len() < offset + 4 {
return None;
}
Some(u32::from_le_bytes(
data[offset..offset + 4].try_into().ok()?,
))
}
fn u64_at(data: &[u8], offset: usize) -> Option<u64> {
if data.len() < offset + 8 {
return None;
}
Some(u64::from_le_bytes(
data[offset..offset + 8].try_into().ok()?,
))
}
fn journal_event_from_bytes(data: &[u8]) -> Option<JournalEvent<TradingEvent>> {
if data.is_empty() {
return None;
}
match data[0] % 11 {
0 => {
Some(JournalEvent::App(TradingEvent::AddInstrument {
spec: InstrumentSpec {
symbol: Symbol(u32_at(data, 1)?),
base: CurrencyId(u32_at(data, 5)?),
quote: CurrencyId(u32_at(data, 9)?),
},
}))
}
1 => {
Some(JournalEvent::App(TradingEvent::Deposit {
account: AccountId(u32_at(data, 1)?),
currency: CurrencyId(u32_at(data, 5)?),
amount: u64_at(data, 9)?,
}))
}
2 => {
if data.len() < 28 {
return None;
}
let symbol = Symbol(u32_at(data, 1)?);
let id = OrderId(u64_at(data, 5)?);
let account = AccountId(u32_at(data, 13)?);
let side = if data[17] & 1 == 0 {
Side::Buy
} else {
Side::Sell
};
let qty = Quantity(nz64(data, 18)?);
let tif = match data[26] % 3 {
0 => TimeInForce::GTC,
1 => TimeInForce::IOC,
_ => TimeInForce::FOK,
};
let stp = match data[27] % 4 {
0 => SelfTradeProtection::Allow,
1 => SelfTradeProtection::CancelNewest,
2 => SelfTradeProtection::CancelOldest,
_ => SelfTradeProtection::CancelBoth,
};
if data.len() < 29 {
return None;
}
let order_type = match data[28] % 4 {
0 => OrderType::Market,
1 => OrderType::Limit {
price: Price(nz64(data, 29)?),
post_only: false,
},
2 => OrderType::Stop {
trigger_price: Price(nz64(data, 29)?),
},
_ => OrderType::StopLimit {
trigger_price: Price(nz64(data, 29)?),
limit_price: Price(nz64(data, 37)?),
},
};
Some(JournalEvent::App(TradingEvent::SubmitOrder {
symbol,
order: Order {
id,
account,
side,
order_type,
time_in_force: tif,
quantity: qty,
stp,
expiry_ns: 0,
},
}))
}
3 => {
Some(JournalEvent::App(TradingEvent::CancelOrder {
symbol: Symbol(u32_at(data, 1)?),
account: AccountId(u32_at(data, 5)?),
order_id: OrderId(u64_at(data, 9)?),
}))
}
4 => {
if data.len() < 7 {
return None;
}
let symbol = Symbol(u32_at(data, 1)?);
let mut p = 5;
let max_order_qty = if data[p] & 1 == 1 {
p += 1;
let v = Quantity(nz64(data, p)?);
p += 8;
Some(v)
} else {
p += 1;
None
};
let max_order_notional = if data.len() > p && data[p] & 1 == 1 {
p += 1;
Some(u64_at(data, p)?)
} else {
None
};
Some(JournalEvent::App(TradingEvent::SetRiskLimits {
symbol,
limits: RiskLimits {
max_order_qty,
max_order_notional,
},
}))
}
5 => {
Some(JournalEvent::App(TradingEvent::CancelAll {
account: AccountId(u32_at(data, 1)?),
}))
}
7 => {
Some(JournalEvent::App(TradingEvent::DisableInstrument {
symbol: Symbol(u32_at(data, 1)?),
}))
}
8 => {
Some(JournalEvent::App(TradingEvent::EnableInstrument {
symbol: Symbol(u32_at(data, 1)?),
}))
}
9 => {
Some(JournalEvent::App(TradingEvent::RemoveInstrument {
symbol: Symbol(u32_at(data, 1)?),
}))
}
_ => {
if data.len() < 7 {
return None;
}
let symbol = Symbol(u32_at(data, 1)?);
let mut p = 5;
let lower = if data[p] & 1 == 1 {
p += 1;
let v = Price(nz64(data, p)?);
p += 8;
Some(v)
} else {
p += 1;
None
};
let upper = if data.len() > p && data[p] & 1 == 1 {
p += 1;
let v = Price(nz64(data, p)?);
p += 8;
Some(v)
} else {
p += 1;
None
};
let halted = data.len() > p && data[p] & 1 == 1;
Some(JournalEvent::App(TradingEvent::SetCircuitBreaker {
symbol,
config: CircuitBreakerConfig {
price_band_lower: lower,
price_band_upper: upper,
halted,
},
}))
}
}
}