use chrono::{NaiveDateTime, TimeZone, Utc};
use crate::isolation::isolation_log::{read_log_tail, LOG_TAIL_BYTES};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LogFooter {
pub exit_code: Option<i32>,
pub finished_at: Option<String>,
}
fn is_footer_separator_line(line: &str) -> bool {
line.len() >= 10 && line.chars().all(|c| c == '=')
}
pub fn parse_footer_timestamp(value: &str) -> Option<String> {
let text = value.trim().trim_end_matches('Z');
if text.is_empty() {
return None;
}
let normalized = text.replacen('T', " ", 1);
let naive = NaiveDateTime::parse_from_str(&normalized, "%Y-%m-%d %H:%M:%S%.f")
.or_else(|_| NaiveDateTime::parse_from_str(&normalized, "%Y-%m-%d %H:%M:%S"))
.ok()?;
Some(Utc.from_utc_datetime(&naive).to_rfc3339())
}
pub fn parse_footer_from_tail(tail: &str) -> LogFooter {
let lines: Vec<&str> = tail
.lines()
.map(|line| line.trim_end_matches('\r'))
.collect();
for index in (2..lines.len()).rev() {
let value = match lines[index].strip_prefix("Exit Code:") {
Some(value) => value,
None => continue,
};
let finished = match lines[index - 1].strip_prefix("Finished:") {
Some(finished) => finished,
None => continue,
};
if !is_footer_separator_line(lines[index - 2]) {
continue;
}
if let Ok(code) = value.trim().parse::<i32>() {
return LogFooter {
exit_code: Some(code),
finished_at: parse_footer_timestamp(finished),
};
}
}
LogFooter::default()
}
pub fn read_footer_from_log(log_path: &str) -> LogFooter {
match read_log_tail(log_path, LOG_TAIL_BYTES) {
Some(tail) => parse_footer_from_tail(&tail),
None => LogFooter::default(),
}
}