story-kit: merge 162_story_colored_server_terminal_log_output
This commit is contained in:
@@ -53,6 +53,17 @@ impl LogEntry {
|
|||||||
pub fn formatted(&self) -> String {
|
pub fn formatted(&self) -> String {
|
||||||
format!("{} [{}] {}", self.timestamp, self.level.as_str(), self.message)
|
format!("{} [{}] {}", self.timestamp, self.level.as_str(), self.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format with ANSI color codes for terminal output.
|
||||||
|
/// WARN is yellow, ERROR is red, INFO has no color.
|
||||||
|
fn colored_formatted(&self) -> String {
|
||||||
|
let line = self.formatted();
|
||||||
|
match self.level {
|
||||||
|
LogLevel::Warn => format!("\x1b[33m{line}\x1b[0m"),
|
||||||
|
LogLevel::Error => format!("\x1b[31m{line}\x1b[0m"),
|
||||||
|
LogLevel::Info => line,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LogBuffer {
|
pub struct LogBuffer {
|
||||||
@@ -74,8 +85,7 @@ impl LogBuffer {
|
|||||||
timestamp,
|
timestamp,
|
||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
let line = entry.formatted();
|
eprintln!("{}", entry.colored_formatted());
|
||||||
eprintln!("{line}");
|
|
||||||
if let Ok(mut buf) = self.entries.lock() {
|
if let Ok(mut buf) = self.entries.lock() {
|
||||||
if buf.len() >= CAPACITY {
|
if buf.len() >= CAPACITY {
|
||||||
buf.pop_front();
|
buf.pop_front();
|
||||||
@@ -295,4 +305,62 @@ mod tests {
|
|||||||
assert_eq!(LogLevel::from_str_ci("info"), Some(LogLevel::Info));
|
assert_eq!(LogLevel::from_str_ci("info"), Some(LogLevel::Info));
|
||||||
assert_eq!(LogLevel::from_str_ci("DEBUG"), None);
|
assert_eq!(LogLevel::from_str_ci("DEBUG"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colored_formatted_warn_has_yellow_ansi() {
|
||||||
|
let entry = LogEntry {
|
||||||
|
level: LogLevel::Warn,
|
||||||
|
timestamp: "2026-01-01T00:00:00Z".into(),
|
||||||
|
message: "test warning".into(),
|
||||||
|
};
|
||||||
|
let colored = entry.colored_formatted();
|
||||||
|
assert!(colored.starts_with("\x1b[33m"), "WARN should start with yellow ANSI code");
|
||||||
|
assert!(colored.ends_with("\x1b[0m"), "WARN should end with ANSI reset");
|
||||||
|
assert!(colored.contains("[WARN]"));
|
||||||
|
assert!(colored.contains("test warning"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colored_formatted_error_has_red_ansi() {
|
||||||
|
let entry = LogEntry {
|
||||||
|
level: LogLevel::Error,
|
||||||
|
timestamp: "2026-01-01T00:00:00Z".into(),
|
||||||
|
message: "test error".into(),
|
||||||
|
};
|
||||||
|
let colored = entry.colored_formatted();
|
||||||
|
assert!(colored.starts_with("\x1b[31m"), "ERROR should start with red ANSI code");
|
||||||
|
assert!(colored.ends_with("\x1b[0m"), "ERROR should end with ANSI reset");
|
||||||
|
assert!(colored.contains("[ERROR]"));
|
||||||
|
assert!(colored.contains("test error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colored_formatted_info_has_no_ansi() {
|
||||||
|
let entry = LogEntry {
|
||||||
|
level: LogLevel::Info,
|
||||||
|
timestamp: "2026-01-01T00:00:00Z".into(),
|
||||||
|
message: "test info".into(),
|
||||||
|
};
|
||||||
|
let colored = entry.colored_formatted();
|
||||||
|
assert!(!colored.contains("\x1b["), "INFO should have no ANSI escape codes");
|
||||||
|
assert!(colored.contains("[INFO]"));
|
||||||
|
assert!(colored.contains("test info"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ring_buffer_entries_have_no_ansi_codes() {
|
||||||
|
let buf = fresh_buffer();
|
||||||
|
buf.push_entry(LogLevel::Info, "info msg".into());
|
||||||
|
buf.push_entry(LogLevel::Warn, "warn msg".into());
|
||||||
|
buf.push_entry(LogLevel::Error, "error msg".into());
|
||||||
|
|
||||||
|
let recent = buf.get_recent(10, None, None);
|
||||||
|
assert_eq!(recent.len(), 3);
|
||||||
|
for line in &recent {
|
||||||
|
assert!(
|
||||||
|
!line.contains("\x1b["),
|
||||||
|
"Ring buffer entry should not contain ANSI codes: {line}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user