diff options
Diffstat (limited to 'mingling_ci/src/res/collect_logs.rs')
| -rw-r--r-- | mingling_ci/src/res/collect_logs.rs | 203 |
1 files changed, 0 insertions, 203 deletions
diff --git a/mingling_ci/src/res/collect_logs.rs b/mingling_ci/src/res/collect_logs.rs deleted file mode 100644 index 6017168..0000000 --- a/mingling_ci/src/res/collect_logs.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! IO side of the report command: reads the collect directory once and keeps -//! the parsed data in a resource, so chains only do computation. - -use std::collections::BTreeMap; - -use mingling::{Program, macros::program_setup}; - -use crate::ThisProgram; -use crate::reporter::COLLECT_DIR; - -/// Git commit date and short hash for the report. -#[derive(Default, Clone, Debug)] -pub struct GitInfo { - pub date: String, - pub commit_hash: String, -} - -/// Parsed contents of the collect directory. -#[derive(Default, Clone)] -pub struct ResCollectLogs { - /// `(task, item) -> os -> ok` - pub statuses: BTreeMap<(String, String), BTreeMap<String, bool>>, - /// `(task, item) -> location` - pub locations: BTreeMap<(String, String), String>, - /// `(task, os, item) -> stripped error output (location line removed)` - pub err_outputs: BTreeMap<(String, String, String), String>, - pub git: GitInfo, -} - -impl ResCollectLogs { - /// Reads the flat `collect/` directory — aggregate `{task}.{os}.ok` files - /// (`item` or `item = location` per line) and per-item - /// `{task}.{os}.{item}.err` files (first line is the location) — plus the - /// git info. - #[must_use] - pub fn read() -> Self { - let mut logs = Self::default(); - - if let Ok(entries) = std::fs::read_dir(COLLECT_DIR) { - for entry in entries.flatten() { - let file_name = entry.file_name().to_string_lossy().into_owned(); - if let Some((task, os)) = parse_ok_name(&file_name) { - // Aggregate success file: `item` or `item = location` per line. - if let Ok(content) = std::fs::read_to_string(entry.path()) { - for line in content.lines().filter(|l| !l.is_empty()) { - let (item, location) = line - .split_once('=') - .map_or((line, ""), |(name, loc)| (name.trim(), loc.trim())); - logs.statuses - .entry((task.clone(), item.to_string())) - .or_default() - .insert(os.clone(), true); - logs.locations - .insert((task.clone(), item.to_string()), location.to_string()); - } - } - } else if let Some((task, os, item)) = parse_err_name(&file_name) { - let content = std::fs::read_to_string(entry.path()).unwrap_or_default(); - let mut lines = content.splitn(2, '\n'); - let location = lines.next().unwrap_or_default().to_string(); - let output = lines.next().unwrap_or_default().to_string(); - logs.statuses - .entry((task.clone(), item.clone())) - .or_default() - .insert(os.clone(), false); - logs.locations - .insert((task.clone(), item.clone()), location); - logs.err_outputs - .insert((task, os, item), strip_ansi(&output)); - } - } - } - - logs.git = git_info(); - logs - } -} - -/// Parses a `{task}.{os}.ok` file name. -fn parse_ok_name(file_name: &str) -> Option<(String, String)> { - let name = file_name.strip_suffix(".ok")?; - let mut parts = name.rsplitn(2, '.'); - let os = parts.next()?.to_string(); - let task = parts.next()?.to_string(); - Some((task, os)) -} - -/// Parses a `{task}.{os}.{package}.err` file name. -/// -/// Split from the right: package names cannot contain dots (cargo forbids -/// them), while task names may. -fn parse_err_name(file_name: &str) -> Option<(String, String, String)> { - let name = file_name.strip_suffix(".err")?; - let mut parts = name.rsplitn(3, '.'); - let package = parts.next()?.to_string(); - let os = parts.next()?.to_string(); - let task = parts.next()?.to_string(); - Some((task, os, package)) -} - -#[program_setup] -pub fn report_setup(p: &mut Program<ThisProgram>) { - p.with_resource(ResCollectLogs::read()); -} - -/// Strips ANSI escape sequences from `input`. -/// -/// Handles CSI (`ESC [ ...`), OSC (`ESC ] ...` terminated by BEL or `ESC \`) -/// and other single-character escapes, while preserving UTF-8 text. Literal -/// `^[` (caret-bracket, produced by some terminal captures) is normalized to -/// `ESC` first. -fn strip_ansi(input: &str) -> String { - // Normalize literal `^[` (0x5E 0x5B) to a real ESC byte. - let normalized = input.replace("^[", "\u{1b}"); - let mut out = String::with_capacity(normalized.len()); - let mut rest = normalized.as_str(); - while let Some(idx) = rest.find('\u{1b}') { - out.push_str(&rest[..idx]); - rest = &rest[idx..]; - rest = &rest[ansi_len(rest)..]; - } - out.push_str(rest); - out -} - -/// Byte length of the ANSI escape sequence starting at `s[0]` (`s[0]` is `ESC`). -fn ansi_len(s: &str) -> usize { - let b = s.as_bytes(); - match b.get(1) { - Some(b'[') => { - // CSI: `ESC [` params/intermediates (0x20-0x3F) then a final byte (0x40-0x7E). - let mut i = 2; - while i < b.len() { - let byte = b[i]; - i += 1; - if (0x40..=0x7E).contains(&byte) { - break; - } - if !(0x20..=0x3F).contains(&byte) { - break; - } - } - i - } - Some(b']') => { - // OSC: `ESC ]` ... terminated by BEL (0x07) or `ESC \`. - let mut i = 2; - while i < b.len() { - let byte = b[i]; - i += 1; - if byte == 0x07 { - break; - } - if byte == 0x1b { - if b.get(i) == Some(&b'\\') { - i += 1; - } - break; - } - } - i - } - Some(_) => 2.min(b.len()), - None => 1, - } -} - -/// Commit date (`YYYY-MM-DD`) and short commit hash; empty on failure. -fn git_info() -> GitInfo { - let run = |args: &[&str]| { - std::process::Command::new("git") - .args(args) - .output() - .ok() - .filter(|o| o.status.success()) - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_default() - }; - GitInfo { - date: run(&["log", "-1", "--format=%cs"]), - commit_hash: run(&["rev-parse", "--short", "HEAD"]), - } -} - -#[cfg(test)] -mod tests { - use super::strip_ansi; - - #[test] - fn strips_csi_and_osc_and_literal_caret() { - let input = - "\u{1b}[1m\u{1b}[92mok\u{1b}[0m \u{1b}]8;;https://x\u{1b}\\done\u{1b}]8;;\u{1b}\\\n"; - assert_eq!(strip_ansi(input), "ok done\n"); - - // Literal `^[` (caret-bracket) captured by some terminals. - assert_eq!(strip_ansi("^[[31mred^[[0m"), "red"); - } - - #[test] - fn preserves_utf8() { - assert_eq!(strip_ansi("你好\u{1b}[1m世界!\u{1b}[0m"), "你好世界!"); - } -} |
