use crate::parser::scope_markers::{self, MarkerKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogEvent {
Invoke {
program_id: String,
depth: u32,
},
Consumed {
program_id: String,
used: u64,
budget: u64,
},
Success {
program_id: String,
},
Failed {
program_id: String,
reason: String,
},
Log {
message: String,
},
ScopeBegin {
name: String,
cu: Option<u64>,
},
ScopeEnd {
name: String,
cu: Option<u64>,
},
ScopePoint {
name: String,
cu: Option<u64>,
},
Raw(String),
}
#[derive(Debug, Default, Clone)]
pub struct LexResult {
pub events: Vec<(usize, LogEvent)>,
pub warnings: Vec<String>,
}
impl LexResult {
pub fn events(&self) -> impl Iterator<Item = &LogEvent> {
self.events.iter().map(|(_, e)| e)
}
}
#[must_use]
pub fn lex(lines: &[String]) -> LexResult {
let mut result = LexResult::default();
for (index, raw) in lines.iter().enumerate() {
let line = raw.trim();
if line.is_empty() {
continue;
}
match classify(line, index) {
Ok(event) => result.events.push((index, event)),
Err(warning) => {
result.warnings.push(warning);
result.events.push((index, LogEvent::Raw(raw.clone())));
}
}
}
result
}
fn classify(line: &str, index: usize) -> Result<LogEvent, String> {
if let Some(rest) = line.strip_prefix("Program log: ") {
return Ok(classify_log_message(rest));
}
if let Some(rest) = line.strip_prefix("Program data: ") {
return Ok(LogEvent::Log {
message: format!("data: {rest}"),
});
}
if line == "Program failed to complete" {
return Ok(LogEvent::Failed {
program_id: String::new(),
reason: "failed to complete".to_string(),
});
}
if let Some(rest) = line.strip_prefix("Program ") {
return classify_program_line(rest, index);
}
Ok(LogEvent::Raw(line.to_string()))
}
fn classify_log_message(message: &str) -> LogEvent {
match scope_markers::parse_marker(message) {
Some(m) => match m.kind {
MarkerKind::Begin => LogEvent::ScopeBegin {
name: m.name,
cu: m.cu,
},
MarkerKind::End => LogEvent::ScopeEnd {
name: m.name,
cu: m.cu,
},
MarkerKind::Point => LogEvent::ScopePoint {
name: m.name,
cu: m.cu,
},
},
None => LogEvent::Log {
message: message.to_string(),
},
}
}
fn classify_program_line(rest: &str, index: usize) -> Result<LogEvent, String> {
if let Some((id, tail)) = rest.split_once(" invoke ") {
let depth = tail
.trim()
.trim_start_matches('[')
.trim_end_matches(']')
.parse::<u32>()
.map_err(|_| {
format!(
"failed to parse invoke depth at log index {index}: expected `[<n>]`, got `{tail}`"
)
})?;
return Ok(LogEvent::Invoke {
program_id: id.to_string(),
depth,
});
}
if let Some((id, tail)) = rest.split_once(" consumed ") {
return parse_consumed(id, tail, index).map_err(|e| e.to_string());
}
if let Some(id) = rest.strip_suffix(" success") {
return Ok(LogEvent::Success {
program_id: id.to_string(),
});
}
if let Some((id, reason)) = rest.split_once(" failed: ") {
return Ok(LogEvent::Failed {
program_id: id.to_string(),
reason: reason.to_string(),
});
}
if let Some(id) = rest.strip_suffix(" failed") {
return Ok(LogEvent::Failed {
program_id: id.to_string(),
reason: "failed".to_string(),
});
}
Ok(LogEvent::Raw(format!("Program {rest}")))
}
fn parse_consumed(id: &str, tail: &str, index: usize) -> crate::Result<LogEvent> {
let body = tail.strip_suffix(" compute units").unwrap_or(tail).trim();
let (used_s, budget_s) = body.split_once(" of ").ok_or_else(|| {
crate::Error::parse(
"compute-unit line",
index,
format!("expected `<used> of <budget> compute units`, got `{tail}`"),
)
})?;
let used = used_s.trim().replace(',', "").parse::<u64>().map_err(|_| {
crate::Error::parse(
"compute-unit line",
index,
format!("expected integer for consumed units, got `{used_s}`"),
)
})?;
let budget = budget_s
.trim()
.replace(',', "")
.parse::<u64>()
.map_err(|_| {
crate::Error::parse(
"compute-unit line",
index,
format!("expected integer for unit budget, got `{budget_s}`"),
)
})?;
Ok(LogEvent::Consumed {
program_id: id.to_string(),
used,
budget,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_invoke_consumed_success() {
let lines = vec![
"Program Vote111 invoke [1]".to_string(),
"Program Vote111 consumed 1200 of 200000 compute units".to_string(),
"Program Vote111 success".to_string(),
];
let r = lex(&lines);
assert!(r.warnings.is_empty());
assert_eq!(
r.events[0].1,
LogEvent::Invoke {
program_id: "Vote111".into(),
depth: 1
}
);
assert_eq!(
r.events[1].1,
LogEvent::Consumed {
program_id: "Vote111".into(),
used: 1200,
budget: 200000
}
);
assert_eq!(
r.events[2].1,
LogEvent::Success {
program_id: "Vote111".into()
}
);
}
#[test]
fn unknown_lines_are_preserved_not_panicked() {
let lines = vec!["totally unexpected line".to_string()];
let r = lex(&lines);
assert_eq!(
r.events[0].1,
LogEvent::Raw("totally unexpected line".into())
);
assert!(r.warnings.is_empty());
}
#[test]
fn malformed_consumed_warns_and_keeps_raw() {
let lines = vec!["Program X consumed abc of 200000 compute units".to_string()];
let r = lex(&lines);
assert_eq!(r.warnings.len(), 1);
assert!(r.warnings[0].contains("expected integer for consumed units"));
assert!(matches!(r.events[0].1, LogEvent::Raw(_)));
}
#[test]
fn detects_failure_with_reason() {
let lines = vec!["Program X failed: custom program error: 0x1".to_string()];
let r = lex(&lines);
assert_eq!(
r.events[0].1,
LogEvent::Failed {
program_id: "X".into(),
reason: "custom program error: 0x1".into()
}
);
}
#[test]
fn detects_scope_markers_in_log_messages() {
let lines = vec![
"Program log: CU_PROFILER_BEGIN name=swap::validate cu=200000".to_string(),
"Program log: hello".to_string(),
"Program log: CU_PROFILER_END name=swap::validate cu=195000".to_string(),
];
let r = lex(&lines);
assert_eq!(
r.events[0].1,
LogEvent::ScopeBegin {
name: "swap::validate".into(),
cu: Some(200_000),
}
);
assert!(matches!(r.events[1].1, LogEvent::Log { .. }));
assert_eq!(
r.events[2].1,
LogEvent::ScopeEnd {
name: "swap::validate".into(),
cu: Some(195_000),
}
);
}
}