use std::{fs, path::Path};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::{
app::{JobEvent, JobState, KeyPress, LOG_READ_MAX, LOG_TICK_MAX},
model::{BuildTarget, MxrunConfig, ResultMirrorPlan},
runner::BuildJob,
};
#[test]
fn job_state_rebuilds_rendered_lines_only_when_log_changes() {
let tmp = std::env::temp_dir().join(format!("mxrun-app-ut-{}-{}", std::process::id(), uniq()));
let logs = tmp.join("logs");
let job = BuildJob::build(
&BuildTarget::local(),
"devel",
&tmp,
&logs,
"make",
&ResultMirrorPlan::disabled(tmp.join("mirror"), "devel"),
);
let log = job.log_path().to_path_buf();
let mut st = JobState::from_job(&job);
fs::create_dir_all(&logs).expect("log dir should exist");
fs::write(&log, "first\n").expect("log should be written");
assert!(st.refresh_log());
assert_eq!(st.log_lines().len(), 2);
assert_eq!(st.log_lines()[0].spans[0].content, "first");
let before = st.log_lines().to_vec();
assert!(!st.refresh_log());
assert_eq!(st.log_lines(), before.as_slice());
fs::write(&log, "first\nsecond\n").expect("log should be appended");
assert!(st.refresh_log());
assert_eq!(st.log_lines().len(), 3);
assert_eq!(st.log_lines()[1].spans[0].content, "second");
fs::remove_dir_all(&tmp).ok();
}
#[test]
fn job_state_keeps_running_stage_until_log_drains() {
let tmp = std::env::temp_dir().join(format!("mxrun-app-ut-{}-{}", std::process::id(), uniq()));
let logs = tmp.join("logs");
let job = BuildJob::build(
&BuildTarget::local(),
"devel",
&tmp,
&logs,
"make",
&ResultMirrorPlan::disabled(tmp.join("mirror"), "devel"),
);
let log = job.log_path().to_path_buf();
let mut st = JobState::from_job(&job);
let text = "x".repeat(LOG_TICK_MAX + 128);
fs::create_dir_all(&logs).expect("log dir should exist");
fs::write(&log, &text).expect("log should be written");
st.apply(JobEvent::building(0));
assert!(st.refresh_log());
st.apply(JobEvent::finished(0, 0));
assert!(!st.is_finished());
assert_eq!(st.summary(), "building");
assert!(st.refresh_log());
assert!(st.is_finished());
assert!(st.is_success());
assert_eq!(st.summary(), "finished");
fs::remove_dir_all(&tmp).ok();
}
#[test]
fn job_state_drains_many_chunks_in_one_tick() {
let tmp = std::env::temp_dir().join(format!("mxrun-app-ut-{}-{}", std::process::id(), uniq()));
let logs = tmp.join("logs");
let job = BuildJob::build(
&BuildTarget::local(),
"devel",
&tmp,
&logs,
"make",
&ResultMirrorPlan::disabled(tmp.join("mirror"), "devel"),
);
let log = job.log_path().to_path_buf();
let mut st = JobState::from_job(&job);
let text = "x".repeat(LOG_READ_MAX * 3 + 7);
fs::create_dir_all(&logs).expect("log dir should exist");
fs::write(&log, &text).expect("log should be written");
assert!(st.refresh_log());
assert_eq!(st.log_lines()[0].spans[0].content.len(), text.len());
fs::remove_dir_all(&tmp).ok();
}
#[test]
fn job_state_updates_rendered_lines_for_error_events() {
let tmp = std::env::temp_dir().join(format!("mxrun-app-ut-{}-{}", std::process::id(), uniq()));
let plan = crate::runner::BuildPlan::new(
&MxrunConfig::parse("local\n").expect("config should parse"),
"devel",
Path::new(&tmp),
&tmp.join("logs"),
"make",
ResultMirrorPlan::disabled(tmp.join("mirror"), "devel"),
);
let mut st = JobState::from_job(&plan.jobs()[0]);
st.apply(JobEvent::failed(0, "boom".to_string()));
assert!(
st.log_lines()
.iter()
.flat_map(|line| line.spans.iter())
.any(|span| span.content.contains("boom"))
);
fs::remove_dir_all(&tmp).ok();
}
#[test]
fn should_quit_finished_accepts_q_key() {
let q = KeyPress::from_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE));
let upper_q = KeyPress::from_key(KeyEvent::new(KeyCode::Char('Q'), KeyModifiers::NONE));
assert!(q.should_quit_finished());
assert!(upper_q.should_quit_finished());
}
#[test]
fn should_quit_finished_accepts_ctrl_c() {
let ctrl_c = KeyPress::from_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert!(ctrl_c.should_quit_finished());
}
fn uniq() -> String {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time should advance")
.as_nanos()
.to_string()
}