use std::io::Cursor;
use super::*;
use crate::record_kind::BinaryLogRecordKind;
fn encode_7bit(value: i32) -> Vec<u8> {
let mut v = value as u32;
let mut buf = Vec::new();
loop {
let mut byte = (v & 0x7F) as u8;
v >>= 7;
if v != 0 {
byte |= 0x80;
}
buf.push(byte);
if v == 0 {
break;
}
}
buf
}
fn encode_i32_le(value: i32) -> Vec<u8> {
value.to_le_bytes().to_vec()
}
fn build_test_binlog() -> Vec<u8> {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_i32_le(18));
let payload = {
let mut p = Vec::new();
p.extend_from_slice(&encode_7bit(0x0004));
p.extend_from_slice(&encode_7bit(0));
p.extend_from_slice(&encode_7bit(0));
p
};
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildStarted as i32));
decompressed.extend_from_slice(&encode_7bit(payload.len() as i32));
decompressed.extend_from_slice(&payload);
let payload2 = {
let mut p = Vec::new();
p.extend_from_slice(&encode_7bit(0x0004));
p.extend_from_slice(&encode_7bit(0));
p.push(1u8);
p
};
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildFinished as i32));
decompressed.extend_from_slice(&encode_7bit(payload2.len() as i32));
decompressed.extend_from_slice(&payload2);
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::EndOfFile as i32));
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
encoder.finish().unwrap()
}
fn build_binlog_with_context() -> Vec<u8> {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_i32_le(18));
let payload = {
let mut p = Vec::new();
p.extend_from_slice(&encode_7bit(0x0004)); p.extend_from_slice(&encode_7bit(0)); p.extend_from_slice(&encode_7bit(0)); p
};
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildStarted as i32));
decompressed.extend_from_slice(&encode_7bit(payload.len() as i32));
decompressed.extend_from_slice(&payload);
let payload2 = {
let mut p = Vec::new();
p.extend_from_slice(&encode_7bit(0x0005));
p.extend_from_slice(&encode_7bit(0));
p.extend_from_slice(&encode_7bit(1)); p.extend_from_slice(&encode_7bit(42)); p.extend_from_slice(&encode_7bit(7)); p.extend_from_slice(&encode_7bit(3)); p.extend_from_slice(&encode_7bit(0)); p.extend_from_slice(&encode_7bit(0)); p.extend_from_slice(&encode_7bit(0)); p.push(1u8);
p
};
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildFinished as i32));
decompressed.extend_from_slice(&encode_7bit(payload2.len() as i32));
decompressed.extend_from_slice(&payload2);
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::EndOfFile as i32));
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
encoder.finish().unwrap()
}
#[test]
fn open_minimal_binlog() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert_eq!(
index.len(),
2,
"expected 2 events (BuildStarted + BuildFinished)"
);
assert!(!index.is_empty());
}
#[test]
fn header_is_correct() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert_eq!(index.header().file_format_version, 18);
assert_eq!(index.header().min_reader_version, 18);
}
#[test]
fn meta_record_kinds() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert_eq!(
index.meta(0).unwrap().record_kind,
BinaryLogRecordKind::BuildStarted
);
assert_eq!(
index.meta(1).unwrap().record_kind,
BinaryLogRecordKind::BuildFinished
);
assert!(index.meta(2).is_none());
}
#[test]
fn meta_byte_offsets_increase() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let off0 = index.meta(0).unwrap().byte_offset;
let off1 = index.meta(1).unwrap().byte_offset;
assert!(off1 > off0, "second event offset must be after first");
}
#[test]
fn meta_payload_len_nonzero() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert!(index.meta(0).unwrap().payload_len > 0);
assert!(index.meta(1).unwrap().payload_len > 0);
}
#[test]
fn get_deserializes_build_started() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let event = index.get(0).expect("deserialize failed").unwrap();
assert!(matches!(event, BinlogEvent::BuildStarted(_)));
}
#[test]
fn get_deserializes_build_finished() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let event = index.get(1).expect("deserialize failed").unwrap();
assert!(matches!(event, BinlogEvent::BuildFinished(_)));
}
#[test]
fn get_out_of_range_returns_none() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert!(index.get(999).expect("should not error").is_none());
}
#[test]
fn get_all_returns_all_events() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let events = index.get_all().expect("get_all failed");
assert_eq!(events.len(), 2);
assert!(matches!(events[0], BinlogEvent::BuildStarted(_)));
assert!(matches!(events[1], BinlogEvent::BuildFinished(_)));
}
#[test]
fn indices_by_kind_filters_correctly() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let started = index.indices_by_kind(BinaryLogRecordKind::BuildStarted);
assert_eq!(started, vec![0]);
let finished = index.indices_by_kind(BinaryLogRecordKind::BuildFinished);
assert_eq!(finished, vec![1]);
let messages = index.indices_by_kind(BinaryLogRecordKind::Message);
assert!(messages.is_empty());
}
#[test]
fn extract_context_from_payload() {
let data = build_binlog_with_context();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert!(index.meta(0).unwrap().context.is_none());
let ctx = index.meta(1).unwrap().context.unwrap();
assert_eq!(ctx.project_context_id, 42);
assert_eq!(ctx.target_id, 7);
assert_eq!(ctx.task_id, 3);
}
#[test]
fn indices_by_project_context() {
let data = build_binlog_with_context();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let matching = index.indices_by_project_context(42);
assert_eq!(matching, vec![1]);
let none = index.indices_by_project_context(999);
assert!(none.is_empty());
}
#[test]
fn indices_by_target_id() {
let data = build_binlog_with_context();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let matching = index.indices_by_target_id(7);
assert_eq!(matching, vec![1]);
}
#[test]
fn indices_by_task_id() {
let data = build_binlog_with_context();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let matching = index.indices_by_task_id(3);
assert_eq!(matching, vec![1]);
}
#[test]
fn query_combined_filters() {
let data = build_binlog_with_context();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let r = index.query(Some(BinaryLogRecordKind::BuildFinished), None, None, None);
assert_eq!(r, vec![1]);
let r = index.query(
Some(BinaryLogRecordKind::BuildFinished),
Some(42),
None,
None,
);
assert_eq!(r, vec![1]);
let r = index.query(Some(BinaryLogRecordKind::Message), Some(42), None, None);
assert!(r.is_empty());
let r = index.query(
Some(BinaryLogRecordKind::BuildFinished),
Some(42),
Some(7),
Some(3),
);
assert_eq!(r, vec![1]);
let r = index.query(
Some(BinaryLogRecordKind::BuildFinished),
Some(42),
Some(7),
Some(999),
);
assert!(r.is_empty());
let r = index.query(None, None, None, None);
assert_eq!(r, vec![0, 1]);
}
#[test]
fn iter_meta_yields_all() {
let data = build_test_binlog();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
let metas: Vec<_> = index.iter_meta().collect();
assert_eq!(metas.len(), 2);
assert_eq!(metas[0].0, 0);
assert_eq!(metas[1].0, 1);
}
#[test]
fn empty_binlog_produces_empty_index() {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::EndOfFile as i32));
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
let data = encoder.finish().unwrap();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert!(index.is_empty());
assert_eq!(index.len(), 0);
assert!(index.get(0).expect("should not error").is_none());
}
#[test]
fn string_table_populated_during_indexing() {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_i32_le(18));
let string_payload = b"hello";
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::String as i32));
decompressed.extend_from_slice(&encode_7bit(string_payload.len() as i32));
decompressed.extend_from_slice(string_payload);
let payload = {
let mut p = Vec::new();
p.extend_from_slice(&encode_7bit(0x0004)); p.extend_from_slice(&encode_7bit(10)); p.extend_from_slice(&encode_7bit(0)); p
};
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildStarted as i32));
decompressed.extend_from_slice(&encode_7bit(payload.len() as i32));
decompressed.extend_from_slice(&payload);
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::EndOfFile as i32));
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
let data = encoder.finish().unwrap();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert_eq!(index.strings().len(), 1);
assert_eq!(index.strings().get(10).unwrap(), Some("hello"));
let event = index.get(0).unwrap().unwrap();
if let BinlogEvent::BuildStarted(ref e) = event {
assert_eq!(e.fields.message.as_deref(), Some("hello"));
} else {
panic!("expected BuildStarted");
}
}
#[test]
fn unknown_record_kind_skipped() {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_7bit(200));
let unknown_payload = vec![0u8; 10];
decompressed.extend_from_slice(&encode_7bit(unknown_payload.len() as i32));
decompressed.extend_from_slice(&unknown_payload);
let payload = {
let mut p = Vec::new();
p.extend_from_slice(&encode_7bit(0x0004));
p.extend_from_slice(&encode_7bit(0));
p.extend_from_slice(&encode_7bit(0)); p
};
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildStarted as i32));
decompressed.extend_from_slice(&encode_7bit(payload.len() as i32));
decompressed.extend_from_slice(&payload);
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::EndOfFile as i32));
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
let data = encoder.finish().unwrap();
let index = BinlogIndex::open(Cursor::new(data)).expect("failed to open");
assert_eq!(index.len(), 1);
assert_eq!(
index.meta(0).unwrap().record_kind,
BinaryLogRecordKind::BuildStarted
);
}
#[test]
fn rejects_negative_record_length() {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18)); decompressed.extend_from_slice(&encode_i32_le(18)); decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildStarted as i32));
decompressed.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
let data = encoder.finish().unwrap();
let err = BinlogIndex::open(Cursor::new(data)).unwrap_err();
assert!(
matches!(err, crate::error::MuninError::InvalidFormat(_)),
"expected InvalidFormat, got {err:?}"
);
}
#[test]
fn rejects_oversized_record_length() {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::BuildStarted as i32));
decompressed.extend_from_slice(&[0x81, 0x80, 0x80, 0x80, 0x01]);
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
let data = encoder.finish().unwrap();
let err = BinlogIndex::open(Cursor::new(data)).unwrap_err();
assert!(
matches!(err, crate::error::MuninError::InvalidFormat(_)),
"expected InvalidFormat, got {err:?}"
);
}
#[test]
fn rejects_negative_nvl_count_in_index() {
use std::io::Write;
use flate2::{write::GzEncoder, Compression};
let payload: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF, 0x0F];
let mut decompressed = Vec::new();
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_i32_le(18));
decompressed.extend_from_slice(&encode_7bit(BinaryLogRecordKind::NameValueList as i32));
decompressed.extend_from_slice(&encode_7bit(payload.len() as i32));
decompressed.extend_from_slice(payload);
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(&decompressed).unwrap();
let data = encoder.finish().unwrap();
let err = BinlogIndex::open(Cursor::new(data)).unwrap_err();
assert!(
matches!(err, crate::error::MuninError::InvalidFormat(_)),
"expected InvalidFormat, got {err:?}"
);
}