use manasight_parser::log::entry::LineBuffer;
use manasight_parser::router::Router;
use manasight_parser::{parse_whole_log, GameEvent};
fn parse_via_router(input: &str) -> Vec<GameEvent> {
let mut buffer = LineBuffer::new();
let router = Router::new();
let mut events = Vec::new();
for line in input.lines() {
for entry in buffer.push_line(line) {
events.extend(router.route(&entry));
}
}
if let Some(entry) = buffer.flush() {
events.extend(router.route(&entry));
}
events
}
#[test]
fn test_parse_whole_log_empty_input_returns_empty_vec() {
let events = parse_whole_log("");
assert!(events.is_empty());
}
#[test]
fn test_parse_whole_log_metadata_parity_with_router() {
let input = "DETAILED LOGS: ENABLED\n";
let via_fn = parse_whole_log(input);
let via_router = parse_via_router(input);
assert_eq!(
via_fn.len(),
via_router.len(),
"parse_whole_log and router path must emit the same number of events"
);
assert_eq!(via_fn.len(), 1);
assert!(matches!(via_fn[0], GameEvent::DetailedLoggingStatus(_)));
}
#[test]
fn test_parse_whole_log_session_event_parity_with_router() {
let input = "[UnityCrossThreadLogger]authenticateResponse\n\
{\"screenName\":\"TestPlayer\"}\n\
[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
some filler\n";
let via_fn = parse_whole_log(input);
let via_router = parse_via_router(input);
assert_eq!(
via_fn.len(),
via_router.len(),
"event count must match between parse_whole_log and router path"
);
assert_eq!(via_fn.len(), 1);
assert!(matches!(via_fn[0], GameEvent::Session(_)));
}
#[test]
fn test_parse_whole_log_multiple_events_parity_with_router() {
let gs_payload = serde_json::json!({
"greToClientEvent": {
"greToClientMessages": [{
"type": "GREMessageType_GameStateMessage",
"gameStateMessage": {
"gameInfo": { "stage": "GameStage_Play" },
"gameObjects": [],
"zones": []
}
}]
}
});
let input = format!(
"DETAILED LOGS: ENABLED\n\
[UnityCrossThreadLogger]authenticateResponse\n\
{{\"screenName\":\"TestPlayer\"}}\n\
[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n{gs_payload}\n\
[UnityCrossThreadLogger]2/25/2026 12:00:01 PM\nfiller\n"
);
let via_fn = parse_whole_log(&input);
let via_router = parse_via_router(&input);
assert_eq!(
via_fn.len(),
via_router.len(),
"event count must match: parse_whole_log={}, router={}",
via_fn.len(),
via_router.len()
);
assert_eq!(via_fn.len(), 3);
assert!(matches!(via_fn[0], GameEvent::DetailedLoggingStatus(_)));
assert!(matches!(via_fn[1], GameEvent::Session(_)));
assert!(matches!(via_fn[2], GameEvent::GameState(_)));
}
#[test]
fn test_parse_whole_log_trailing_entry_not_followed_by_header_parity() {
let input = "[UnityCrossThreadLogger]authenticateResponse\n\
{\"screenName\":\"TrailingEntry\"}\n";
let via_fn = parse_whole_log(input);
let via_router = parse_via_router(input);
assert_eq!(
via_fn.len(),
via_router.len(),
"trailing entry must be drained by flush(): parse_whole_log={}, router={}",
via_fn.len(),
via_router.len()
);
assert_eq!(
via_fn.len(),
1,
"expected exactly one event from trailing entry"
);
assert!(matches!(via_fn[0], GameEvent::Session(_)));
}
#[test]
fn test_parse_whole_log_unrecognized_entries_parity_with_router() {
let input = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
some completely unrecognized content\n\
[UnityCrossThreadLogger]2/25/2026 12:00:01 PM\n\
more unrecognized content\n";
let via_fn = parse_whole_log(input);
let via_router = parse_via_router(input);
assert_eq!(
via_fn.len(),
via_router.len(),
"unrecognized entries must produce identical (empty) results"
);
assert!(via_fn.is_empty());
}
fn collect_log_entries(input: &str) -> Vec<manasight_parser::log::entry::LogEntry> {
let mut buffer = LineBuffer::new();
let mut entries = Vec::new();
for line in input.lines() {
entries.extend(buffer.push_line(line));
}
if let Some(entry) = buffer.flush() {
entries.push(entry);
}
entries
}
#[test]
fn test_frame_prefixed_fixture_log_entries_byte_identical_to_unprefixed() {
let unprefixed = include_str!("fixtures/flush_timing_corpus_slice.log");
let clean_unprefixed: String = unprefixed
.lines()
.filter(|line| !line.starts_with('#'))
.fold(String::new(), |mut s, line| {
use std::fmt::Write as _;
let _ = writeln!(s, "{line}");
s
});
let prefixed: String =
clean_unprefixed
.lines()
.enumerate()
.fold(String::new(), |mut s, (n, line)| {
use std::fmt::Write as _;
let _ = writeln!(s, "[{n}] {line}");
s
});
let entries_unprefixed = collect_log_entries(&clean_unprefixed);
let entries_prefixed = collect_log_entries(&prefixed);
assert!(
!entries_unprefixed.is_empty(),
"fixture must yield at least one LogEntry — verify fixture path is correct",
);
assert_eq!(
entries_prefixed.len(),
entries_unprefixed.len(),
"frame-prefixed log must yield the same LogEntry count as the unprefixed original \
(got {}, expected {})",
entries_prefixed.len(),
entries_unprefixed.len(),
);
assert_eq!(
entries_prefixed, entries_unprefixed,
"frame-prefixed log must produce a byte-identical LogEntry stream to the unprefixed original",
);
}
#[test]
fn test_parse_whole_log_frame_prefixed_produces_same_game_events_as_unprefixed() {
let unprefixed = include_str!("fixtures/deck_submission_v2_constructed.log");
let prefixed: String =
unprefixed
.lines()
.enumerate()
.fold(String::new(), |mut s, (n, line)| {
use std::fmt::Write as _;
let _ = writeln!(s, "[{n}] {line}");
s
});
let events_unprefixed = parse_whole_log(unprefixed);
let events_prefixed = parse_whole_log(&prefixed);
assert!(
!events_unprefixed.is_empty(),
"unprefixed fixture must yield at least one GameEvent",
);
assert_eq!(
events_prefixed.len(),
events_unprefixed.len(),
"frame-prefixed log must yield the same GameEvent count as the unprefixed original \
(got {}, expected {})",
events_prefixed.len(),
events_unprefixed.len(),
);
assert_eq!(
events_prefixed, events_unprefixed,
"frame-prefixed log must produce a byte-identical GameEvent stream to the unprefixed original",
);
}