use std::fs;
use tempfile::TempDir;
use super::test_support::{frames, index, segments};
use super::*;
use crate::messages::encode_raw_length;
fn index_without_timestamps(dir: &std::path::Path) -> Vec<(usize, u64, u32)> {
index(dir)
.iter()
.map(|line| {
let fields: Vec<&str> = line.split(',').collect();
assert_eq!(fields.len(), 4, "index line must have four columns: {line:?}");
(fields[0].parse().unwrap(), fields[2].parse().unwrap(), fields[3].parse().unwrap())
})
.collect()
}
fn record_frame(tap: &RawFrameTap, body: &[u8]) {
tap.record_length_prefix(&(body.len() as u32).to_be_bytes());
tap.record_body(body);
}
#[test]
fn test_disabled_tap_is_inert() {
let tap = RawFrameTap::disabled();
assert!(tap.sink.is_none(), "disabled() must hold no sink to write through");
record_frame(&tap, &[1, 2, 3, 4, 5]);
tap.start_new_segment();
}
#[test]
fn test_unset_env_var_disables_the_tap() {
temp_env::with_var_unset("IBAPI_RAW_CAPTURE_DIR", || {
assert!(RawFrameTap::from_env().sink.is_none());
});
temp_env::with_var("IBAPI_RAW_CAPTURE_DIR", Some(""), || {
assert!(RawFrameTap::from_env().sink.is_none());
});
}
#[test]
fn test_env_var_opens_a_capture_under_the_named_directory() {
let dir = TempDir::new().unwrap();
let nested = dir.path().join("does/not/exist/yet");
temp_env::with_var("IBAPI_RAW_CAPTURE_DIR", Some(nested.to_str().unwrap()), || {
let tap = RawFrameTap::from_env();
assert!(tap.sink.is_some());
record_frame(&tap, &[0, 0, 0, 9]);
assert_eq!(frames(&nested), encode_raw_length(&[0, 0, 0, 9]));
});
}
const REPLAY_BODIES: [&[u8]; 3] = [&[0, 0, 0, 9], &[0, 0, 0, 5, 1, 2, 3], &[0, 0, 0, 63, 8, 208, 70]];
fn capture_replay_bodies(dir: &std::path::Path) -> Vec<u8> {
let tap = RawFrameTap::capturing_to(dir);
for body in REPLAY_BODIES {
record_frame(&tap, body);
}
frames(dir)
}
#[cfg(feature = "sync")]
#[test]
fn test_capture_replays_through_the_blocking_frame_reader() {
let dir = TempDir::new().unwrap();
let capture = capture_replay_bodies(dir.path());
let mut replay = capture.as_slice();
for body in REPLAY_BODIES {
let frame = crate::transport::sync::read_message(&mut replay, &RawFrameTap::disabled()).expect("captured frame must replay");
assert_eq!(frame, body);
}
assert!(replay.is_empty(), "replay must consume the capture exactly");
}
#[cfg(feature = "async")]
#[tokio::test]
async fn test_capture_replays_through_the_async_frame_reader() {
use crate::transport::r#async::read_framed_message;
let dir = TempDir::new().unwrap();
let capture = capture_replay_bodies(dir.path());
let mut replay = std::io::Cursor::new(capture.clone());
for body in REPLAY_BODIES {
let frame = read_framed_message(&mut replay, &RawFrameTap::disabled())
.await
.expect("captured frame must replay");
assert_eq!(frame, body);
}
assert_eq!(replay.position() as usize, capture.len(), "replay must consume the capture exactly");
}
#[test]
fn test_prefix_without_a_body_is_still_captured() {
let dir = TempDir::new().unwrap();
let tap = RawFrameTap::capturing_to(dir.path());
record_frame(&tap, &[0, 0, 0, 9]);
tap.record_length_prefix(&u32::MAX.to_be_bytes());
let mut expected = encode_raw_length(&[0, 0, 0, 9]);
expected.extend_from_slice(&u32::MAX.to_be_bytes());
assert_eq!(frames(dir.path()), expected);
assert_eq!(index_without_timestamps(dir.path()), vec![(0, 0, 4), (1, 8, u32::MAX)]);
}
#[test]
fn test_index_offsets_locate_each_prefix_in_the_capture() {
let dir = TempDir::new().unwrap();
let capture = capture_replay_bodies(dir.path());
for (seq, offset, declared) in index_without_timestamps(dir.path()) {
let offset = offset as usize;
assert_eq!(
u32::from_be_bytes(capture[offset..offset + 4].try_into().unwrap()),
declared,
"index row {seq} must point at its own length prefix"
);
}
}
#[test]
fn test_index_timestamps_are_utc_and_ordered() {
let dir = TempDir::new().unwrap();
let tap = RawFrameTap::capturing_to(dir.path());
tap.record_length_prefix(&[0, 0, 0, 4]);
tap.record_length_prefix(&[0, 0, 0, 4]);
let stamps: Vec<String> = index(dir.path()).iter().map(|line| line.split(',').nth(1).unwrap().to_string()).collect();
assert_eq!(stamps.len(), 2);
for stamp in &stamps {
assert!(stamp.ends_with('Z'), "timestamp must be marked UTC: {stamp}");
assert!(stamp.contains('T'), "timestamp must be ISO-8601: {stamp}");
}
assert!(stamps[0] <= stamps[1], "timestamps must not go backwards: {stamps:?}");
}
#[test]
fn test_reconnect_starts_a_new_segment() {
let dir = TempDir::new().unwrap();
let tap = RawFrameTap::capturing_to(dir.path());
record_frame(&tap, &[0, 0, 0, 9]);
tap.start_new_segment();
record_frame(&tap, &[0, 0, 0, 5]);
let bins = segments(dir.path(), "bin");
assert_eq!(bins.len(), 2, "each connection gets its own file: {bins:?}");
assert_eq!(fs::read(&bins[0]).unwrap(), encode_raw_length(&[0, 0, 0, 9]));
assert_eq!(fs::read(&bins[1]).unwrap(), encode_raw_length(&[0, 0, 0, 5]));
let per_segment: Vec<Vec<String>> = segments(dir.path(), "idx")
.iter()
.map(|path| fs::read_to_string(path).unwrap().lines().map(String::from).collect())
.collect();
for lines in &per_segment {
assert_eq!(lines.len(), 1);
assert!(lines[0].starts_with("0,"), "sequence restarts per segment: {lines:?}");
assert!(lines[0].ends_with(",0,4"), "offset restarts per segment: {lines:?}");
}
}
#[test]
fn test_clones_share_one_capture() {
let dir = TempDir::new().unwrap();
let tap = RawFrameTap::capturing_to(dir.path());
let clone = tap.clone();
record_frame(&tap, &[0, 0, 0, 9]);
record_frame(&clone, &[0, 0, 0, 5]);
assert_eq!(segments(dir.path(), "bin").len(), 1, "a clone must not open its own file");
let mut expected = encode_raw_length(&[0, 0, 0, 9]);
expected.extend_from_slice(&encode_raw_length(&[0, 0, 0, 5]));
assert_eq!(frames(dir.path()), expected);
}
#[test]
fn test_unusable_directory_downgrades_to_disabled() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("not-a-directory");
fs::write(&file, b"occupied").unwrap();
let tap = RawFrameTap::capturing_to(file.join("capture"));
assert!(tap.sink.is_none(), "a destination that cannot be opened yields no sink at all");
record_frame(&tap, &[0, 0, 0, 9]);
tap.start_new_segment();
}
#[test]
fn test_a_failed_segment_open_gives_up_permanently() {
let dir = TempDir::new().unwrap();
let capture_dir = dir.path().join("capture");
let tap = RawFrameTap::capturing_to(&capture_dir);
record_frame(&tap, &[0, 0, 0, 9]);
fs::remove_dir_all(&capture_dir).unwrap();
tap.start_new_segment();
let sink = tap.sink.as_ref().expect("the tap outlives its destination");
assert!(sink.lock().segment.is_none(), "a failed open must retire the sink");
record_frame(&tap, &[0, 0, 0, 9]);
tap.start_new_segment();
assert!(sink.lock().segment.is_none(), "a retired sink must not reopen");
}
#[test]
fn test_tap_is_send_and_sync() {
crate::tests::assert_send_and_sync::<RawFrameTap>();
}