use super::*;
fn write_line(path: &Path, line: &str) {
use std::io::Write as _;
let mut file = File::options()
.create(true)
.append(true)
.open(path)
.unwrap();
file.write_all(line.as_bytes()).unwrap();
}
#[test]
fn a_stream_records_a_build_from_start_to_totals() {
let store = tempfile::tempdir().unwrap();
let writer = EventWriter::new(store.path());
writer.started(Path::new("/workspace"), &["build".into()]);
writer.action(
ActionOutcome::Hit,
Some("serde".into()),
11,
ActionDetail {
avoided_compiler_ns: 1_000,
output_files: 2,
output_bytes: 64,
reflinked_output_bytes: 64,
copied_output_bytes: 0,
},
);
writer.action(
ActionOutcome::Bypass {
reason: "incremental".into(),
},
Some("mbx".into()),
22,
ActionDetail::default(),
);
writer.finished(serde_json::json!({ "hits": 1, "misses": 0 }));
let paths = session_paths(store.path(), writer.id());
let contents = std::fs::read_to_string(&paths.events).unwrap();
let events = parse_events(&contents);
assert_eq!(events.len(), 4);
assert!(matches!(
&events[0],
SessionEvent::SessionStarted { command, .. } if command == &["build".to_string()]
));
assert!(matches!(
&events[1],
SessionEvent::Action {
outcome: ActionOutcome::Hit,
crate_name: Some(name),
duration_ns: 11,
detail,
..
} if name == "serde" && detail.output_bytes == 64
));
assert_eq!(events[2].outcome_label(), Some("incremental"));
assert!(matches!(
&events[3],
SessionEvent::SessionFinished { stats, .. } if stat(stats, "hits") == 1
));
}
#[test]
fn a_command_that_records_nothing_leaves_no_stream() {
let store = tempfile::tempdir().unwrap();
let writer = EventWriter::new(store.path());
let paths = session_paths(store.path(), writer.id());
drop(writer);
assert!(!paths.events.exists());
assert!(session_ids(store.path()).is_empty());
}
#[test]
fn a_tail_reads_only_what_was_appended() {
let store = tempfile::tempdir().unwrap();
let writer = EventWriter::new(store.path());
writer.started(Path::new("/workspace"), &["build".into()]);
let id = writer.id().to_string();
let mut tail = SessionTail::new(store.path(), id);
assert_eq!(tail.read().len(), 1);
assert!(tail.read().is_empty());
writer.action(ActionOutcome::Miss, None, 5, ActionDetail::default());
let appended = tail.read();
assert_eq!(appended.len(), 1);
assert_eq!(appended[0].outcome_label(), Some("miss"));
}
#[test]
fn a_tail_holds_back_a_line_that_has_no_newline_yet() {
let store = tempfile::tempdir().unwrap();
std::fs::create_dir_all(store.path().join(SESSIONS_DIR)).unwrap();
let paths = session_paths(store.path(), "1-1-aaaa");
let complete = r#"{"type":"action","v":1,"ts_ms":1,"outcome":{"kind":"miss"},"duration_ns":1}"#;
write_line(&paths.events, &format!("{complete}\n"));
write_line(&paths.events, r#"{"type":"action","v":1,"ts_"#);
let mut tail = SessionTail::new(store.path(), "1-1-aaaa".into());
assert_eq!(tail.read().len(), 1);
write_line(
&paths.events,
"ms\":2,\"outcome\":{\"kind\":\"hit\"},\"duration_ns\":2}\n",
);
let appended = tail.read();
assert_eq!(appended.len(), 1);
assert_eq!(appended[0].outcome_label(), Some("hit"));
}
#[test]
fn a_reader_skips_a_line_it_cannot_parse() {
let store = tempfile::tempdir().unwrap();
std::fs::create_dir_all(store.path().join(SESSIONS_DIR)).unwrap();
let paths = session_paths(store.path(), "1-1-aaaa");
write_line(&paths.events, "not json at all\n");
write_line(
&paths.events,
"{\"type\":\"action\",\"v\":1,\"ts_ms\":1,\"outcome\":{\"kind\":\"hit\"},\"duration_ns\":1}\n",
);
write_line(&paths.events, "{\"type\":\"from_the_future\",\"v\":9}\n");
let mut tail = SessionTail::new(store.path(), "1-1-aaaa".into());
let events = tail.read();
assert_eq!(events.len(), 1);
assert_eq!(events[0].outcome_label(), Some("hit"));
}
#[test]
fn an_event_keeps_its_meaning_when_a_newer_field_is_added() {
let line = r#"{"type":"action","v":1,"ts_ms":1,"outcome":{"kind":"hit"},"duration_ns":7,"crate_name":"serde","invented_later":{"a":1}}"#;
let events = parse_events(line);
assert_eq!(events.len(), 1);
assert!(matches!(
&events[0],
SessionEvent::Action {
crate_name: Some(name),
duration_ns: 7,
..
} if name == "serde"
));
}
#[test]
fn a_live_stream_is_told_apart_from_a_finished_and_an_abandoned_one() {
let store = tempfile::tempdir().unwrap();
let writer = EventWriter::new(store.path());
writer.started(Path::new("/workspace"), &["build".into()]);
let mut live = SessionTail::new(store.path(), writer.id().to_string());
live.read();
assert_eq!(live.state(), SessionState::Live);
writer.finished(serde_json::json!({ "hits": 0 }));
let mut finished = SessionTail::new(store.path(), writer.id().to_string());
finished.read();
assert_eq!(finished.state(), SessionState::Finished);
let abandoned = EventWriter::new(store.path());
abandoned.started(Path::new("/workspace"), &["build".into()]);
let id = abandoned.id().to_string();
drop(abandoned);
let mut tail = SessionTail::new(store.path(), id);
tail.read();
assert_eq!(tail.state(), SessionState::Abandoned);
}
#[test]
fn a_stream_stops_growing_at_its_cap_but_still_reports_its_totals() {
let store = tempfile::tempdir().unwrap();
let writer = EventWriter::with_cap(store.path(), 1);
writer.started(Path::new("/workspace"), &["build".into()]);
for _ in 0..5 {
writer.action(
ActionOutcome::Miss,
Some("serde".into()),
1,
ActionDetail::default(),
);
}
writer.finished(serde_json::json!({ "hits": 3 }));
let paths = session_paths(store.path(), writer.id());
let events = parse_events(&std::fs::read_to_string(&paths.events).unwrap());
assert_eq!(
events
.iter()
.filter(|event| matches!(event, SessionEvent::Truncated { .. }))
.count(),
1
);
assert!(
!events
.iter()
.any(|event| matches!(event, SessionEvent::Action { .. }))
);
assert!(matches!(
events.last(),
Some(SessionEvent::SessionFinished { stats, .. }) if stat(stats, "hits") == 3
));
}
#[test]
fn tails_open_newest_first_and_no_more_than_asked() {
let store = tempfile::tempdir().unwrap();
std::fs::create_dir_all(store.path().join(SESSIONS_DIR)).unwrap();
for id in ["100-1-aaaa", "200-1-bbbb", "300-1-cccc"] {
write_line(&session_paths(store.path(), id).events, "\n");
}
let tails = open_tails(store.path(), 2);
let ids: Vec<&str> = tails.iter().map(SessionTail::id).collect();
assert_eq!(ids, ["300-1-cccc", "200-1-bbbb"]);
}