diff --git a/candumpr/src/format.rs b/candumpr/src/format.rs index dd1d29f..acd06c8 100644 --- a/candumpr/src/format.rs +++ b/candumpr/src/format.rs @@ -1,11 +1,99 @@ use std::io::Write; +use std::time::Duration; +use crate::errframe::ErrorFrame; use crate::frame::CanFrame; +use crate::recv::Timestamp; + +/// How candumpr renders the timestamp prefix on candump-format output. +/// +/// Only the candump formats consult this. ASC and PCAP carry their own intrinsic timestamps. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)] +pub enum TimestampMode { + /// The frame's absolute receive time + #[default] + Absolute, + /// Time since the previous frame + Delta, + /// Time since the first frame + Zero, +} + +/// A resolved timestamp ready to render +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DisplayTimestamp { + /// The frame's absolute receive time. + Absolute(Timestamp), + /// The elapsed duration since the reference frame. + Relative(Duration), +} + +/// Tracks the reference timestamp needed to render relative timestamps ([TimestampMode::Delta] and +/// [TimestampMode::Zero]) +struct TimestampClock { + mode: TimestampMode, + /// For Zero: the first frame's time. For Delta: the previous frame's time. + reference: Option, +} + +impl TimestampClock { + fn new(mode: TimestampMode) -> Self { + Self { + mode, + reference: None, + } + } + + /// Resolve the timestamp to render for `ts`, advancing internal state as needed. + /// + /// The first frame in a relative mode resolves to a zero duration, matching candump. + fn resolve(&mut self, ts: Timestamp) -> DisplayTimestamp { + match self.mode { + TimestampMode::Absolute => DisplayTimestamp::Absolute(ts), + TimestampMode::Zero => { + let first = *self.reference.get_or_insert(ts); + DisplayTimestamp::Relative(elapsed(first, ts)) + } + TimestampMode::Delta => { + let prev = self.reference.replace(ts).unwrap_or(ts); + DisplayTimestamp::Relative(elapsed(prev, ts)) + } + } + } +} + +/// Non-negative elapsed duration from `start` to `end`. +/// +/// Clamps to zero if `end` precedes `start` (e.g. a backward system-clock jump). +fn elapsed(start: Timestamp, end: Timestamp) -> Duration { + let start_ns = start.sec as i128 * 1_000_000_000 + start.nsec as i128; + let end_ns = end.sec as i128 * 1_000_000_000 + end.nsec as i128; + let diff_ns = (end_ns - start_ns).max(0) as u128; + Duration::new( + (diff_ns / 1_000_000_000) as u64, + (diff_ns % 1_000_000_000) as u32, + ) +} + +/// Write the candump-style `(seconds.microseconds) ` timestamp prefix, including the trailing space. +/// +/// Absolute times print unpadded seconds; relative times zero-pad seconds to three digits, matching +/// candump's `(%llu.%06llu)` and `(%03llu.%06llu)` respectively. +fn write_timestamp(buf: &mut Vec, ts: DisplayTimestamp) { + match ts { + DisplayTimestamp::Absolute(t) => { + write!(buf, "({}.{:06}) ", t.sec, t.nsec / 1000).unwrap(); + } + DisplayTimestamp::Relative(d) => { + write!(buf, "({:03}.{:06}) ", d.as_secs(), d.subsec_micros()).unwrap(); + } + } +} /// Formats received CAN frames into bytes for writing. pub trait Formatter { /// Append the formatted representation of `frame` to `buf`. - fn format(&self, frame: &CanFrame, buf: &mut Vec); + fn format(&mut self, frame: &CanFrame, buf: &mut Vec); /// Optional header bytes written once at the start of each output stream, before any frames. /// @@ -15,45 +103,46 @@ pub trait Formatter { } } -/// Formats frames in the can-utils candump file format. +/// The display CAN ID (with flags masked appropriately) and the zero-padded hex width to render it. /// -/// Output format: `(SECONDS.MICROSECONDS) IFACE CANID#DATA\n` +/// Error frames keep the error flag and render at width 8; extended frames render at width 8; +/// standard frames render at width 3. +fn canid_and_width(can_id: u32) -> (u32, usize) { + if can_id & libc::CAN_ERR_FLAG != 0 { + (can_id & (libc::CAN_ERR_MASK | libc::CAN_ERR_FLAG), 8) + } else if can_id & libc::CAN_EFF_FLAG != 0 { + (can_id & libc::CAN_EFF_MASK, 8) + } else { + (can_id & libc::CAN_SFF_MASK, 3) + } +} + +/// Formats frames in the can-utils candump file (log) format. /// -/// Example: `(1616161616.123456) can0 18FECA00#AABB0011` -pub struct CanutilsFormatter { +/// Output format: `(TIMESTAMP) IFACE CANID#DATA\n`, e.g. `(1616161616.123456) can0 18FECA00#AABB0011`. +/// The timestamp is rendered according to the configured [TimestampMode]. +pub struct CanutilsFileFormatter { iface_names: Vec, + clock: TimestampClock, } -impl CanutilsFormatter { - pub fn new(iface_names: Vec) -> Self { - Self { iface_names } +impl CanutilsFileFormatter { + pub fn new(iface_names: Vec, timestamp: TimestampMode) -> Self { + Self { + iface_names, + clock: TimestampClock::new(timestamp), + } } } -impl Formatter for CanutilsFormatter { - fn format(&self, frame: &CanFrame, buf: &mut Vec) { - let ts = &frame.timestamp; +impl Formatter for CanutilsFileFormatter { + fn format(&mut self, frame: &CanFrame, buf: &mut Vec) { + let display = self.clock.resolve(frame.timestamp); + write_timestamp(buf, display); + let iface = &self.iface_names[frame.sock_id]; - let can_id = frame.raw.can_id; - - let (id, width) = if can_id & libc::CAN_ERR_FLAG != 0 { - (can_id & (libc::CAN_ERR_MASK | libc::CAN_ERR_FLAG), 8) - } else if can_id & libc::CAN_EFF_FLAG != 0 { - (can_id & libc::CAN_EFF_MASK, 8) - } else { - (can_id & libc::CAN_SFF_MASK, 3) - }; - - write!( - buf, - "({}.{:06}) {} {:0width$X}#", - ts.sec, - ts.nsec / 1000, - iface, - id, - width = width, - ) - .unwrap(); + let (id, width) = canid_and_width(frame.raw.can_id); + write!(buf, "{iface} {id:0width$X}#").unwrap(); for i in 0..frame.raw.len as usize { write!(buf, "{:02X}", frame.raw.data[i]).unwrap(); } @@ -61,6 +150,48 @@ impl Formatter for CanutilsFormatter { } } +/// Formats frames in a human-readable, candump-console-derived format. +/// +/// We try to match can-utils console format, but I'm not going to try to reproduce every quirk of +/// its spacing. +/// +/// Output format: `(TIMESTAMP) IFACE CANID [LEN] B0 B1 ...`, e.g. +/// `(1616161616.123456) can0 18FECA00 [4] AA BB 00 11`. +/// +/// Error frames append `ERRORFRAME` and a decoded description on the following, tab-indented line. +pub struct CanutilsConsoleFormatter { + iface_names: Vec, + clock: TimestampClock, +} + +impl CanutilsConsoleFormatter { + pub fn new(iface_names: Vec, timestamp: TimestampMode) -> Self { + Self { + iface_names, + clock: TimestampClock::new(timestamp), + } + } +} + +impl Formatter for CanutilsConsoleFormatter { + fn format(&mut self, frame: &CanFrame, buf: &mut Vec) { + let display = self.clock.resolve(frame.timestamp); + write_timestamp(buf, display); + + let iface = &self.iface_names[frame.sock_id]; + let (id, width) = canid_and_width(frame.raw.can_id); + let len = frame.raw.len as usize; + write!(buf, "{iface} {id:0width$X} [{len}]").unwrap(); + for i in 0..len { + write!(buf, " {:02X}", frame.raw.data[i]).unwrap(); + } + if let Some(err) = ErrorFrame::parse(&frame.raw) { + write!(buf, " ERRORFRAME\n\t{err}").unwrap(); + } + buf.push(b'\n'); + } +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; @@ -70,21 +201,32 @@ mod tests { use crate::frame::Direction; use crate::recv::Timestamp; + fn ts(sec: i64, nsec: i64) -> Timestamp { + Timestamp { sec, nsec } + } + + fn frame(sock_id: usize, timestamp: Timestamp, can_id: u32, data: &[u8]) -> CanFrame { + CanFrame { + sock_id, + timestamp, + direction: Direction::Rx, + raw: LinuxCanFrame::new(can_id, data), + } + } + #[test] fn canutils_format_basic() { - let frame = CanFrame { - sock_id: 0, - timestamp: Timestamp { - sec: 1616161616, - nsec: 123000, - }, - direction: Direction::Rx, - raw: LinuxCanFrame::new(0x18FECA00 | libc::CAN_EFF_FLAG, &[0xAA, 0xBB, 0x00, 0x11]), - }; - let names = vec!["can0".to_string()]; - let fmt = CanutilsFormatter::new(names); + let mut fmt = CanutilsFileFormatter::new(vec!["can0".to_string()], TimestampMode::Absolute); let mut buf = Vec::new(); - fmt.format(&frame, &mut buf); + fmt.format( + &frame( + 0, + ts(1616161616, 123000), + 0x18FECA00 | libc::CAN_EFF_FLAG, + &[0xAA, 0xBB, 0x00, 0x11], + ), + &mut buf, + ); assert_eq!( String::from_utf8(buf).unwrap(), "(1616161616.000123) can0 18FECA00#AABB0011\n" @@ -93,16 +235,15 @@ mod tests { #[test] fn canutils_format_empty_data() { - let frame = CanFrame { - sock_id: 1, - timestamp: Timestamp { sec: 0, nsec: 0 }, - direction: Direction::Rx, - raw: LinuxCanFrame::new(0x123 | libc::CAN_EFF_FLAG, &[]), - }; - let names = vec!["vcan0".to_string(), "vcan1".to_string()]; - let fmt = CanutilsFormatter::new(names); + let mut fmt = CanutilsFileFormatter::new( + vec!["vcan0".to_string(), "vcan1".to_string()], + TimestampMode::Absolute, + ); let mut buf = Vec::new(); - fmt.format(&frame, &mut buf); + fmt.format( + &frame(1, ts(0, 0), 0x123 | libc::CAN_EFF_FLAG, &[]), + &mut buf, + ); assert_eq!( String::from_utf8(buf).unwrap(), "(0.000000) vcan1 00000123#\n" @@ -111,16 +252,12 @@ mod tests { #[test] fn canutils_format_error_frame_keeps_err_flag() { - let frame = CanFrame { - sock_id: 0, - timestamp: Timestamp { sec: 1, nsec: 0 }, - direction: Direction::Rx, - raw: LinuxCanFrame::new(libc::CAN_ERR_FLAG | 0x40, &[0, 0, 0, 0, 0, 0, 0, 0]), - }; - let names = vec!["can0".to_string()]; - let fmt = CanutilsFormatter::new(names); + let mut fmt = CanutilsFileFormatter::new(vec!["can0".to_string()], TimestampMode::Absolute); let mut buf = Vec::new(); - fmt.format(&frame, &mut buf); + fmt.format( + &frame(0, ts(1, 0), libc::CAN_ERR_FLAG | 0x40, &[0; 8]), + &mut buf, + ); assert_eq!( String::from_utf8(buf).unwrap(), "(1.000000) can0 20000040#0000000000000000\n" @@ -129,16 +266,145 @@ mod tests { #[test] fn canutils_format_standard_frame_uses_three_digits() { - let frame = CanFrame { - sock_id: 0, - timestamp: Timestamp { sec: 0, nsec: 0 }, - direction: Direction::Rx, - raw: LinuxCanFrame::new(0x123, &[0xAB]), - }; - let names = vec!["can0".to_string()]; - let fmt = CanutilsFormatter::new(names); + let mut fmt = CanutilsFileFormatter::new(vec!["can0".to_string()], TimestampMode::Absolute); let mut buf = Vec::new(); - fmt.format(&frame, &mut buf); + fmt.format(&frame(0, ts(0, 0), 0x123, &[0xAB]), &mut buf); assert_eq!(String::from_utf8(buf).unwrap(), "(0.000000) can0 123#AB\n"); } + + #[test] + fn canutils_format_delta_timestamps() { + let mut fmt = CanutilsFileFormatter::new(vec!["can0".to_string()], TimestampMode::Delta); + let mut buf = Vec::new(); + fmt.format(&frame(0, ts(100, 0), 0x123, &[0xAB]), &mut buf); + fmt.format(&frame(0, ts(100, 250_000_000), 0x123, &[0xAB]), &mut buf); + assert_eq!( + String::from_utf8(buf).unwrap(), + "(000.000000) can0 123#AB\n(000.250000) can0 123#AB\n" + ); + } + + #[test] + fn canutils_format_zero_timestamps() { + let mut fmt = CanutilsFileFormatter::new(vec!["can0".to_string()], TimestampMode::Zero); + let mut buf = Vec::new(); + fmt.format(&frame(0, ts(100, 0), 0x123, &[0xAB]), &mut buf); + fmt.format(&frame(0, ts(105, 0), 0x123, &[0xAB]), &mut buf); + assert_eq!( + String::from_utf8(buf).unwrap(), + "(000.000000) can0 123#AB\n(005.000000) can0 123#AB\n" + ); + } + + #[test] + fn absolute_returns_frame_time_unchanged() { + let mut clock = TimestampClock::new(TimestampMode::Absolute); + assert_eq!( + clock.resolve(ts(1_700_000_000, 123_456_000)), + DisplayTimestamp::Absolute(ts(1_700_000_000, 123_456_000)) + ); + } + + #[test] + fn zero_is_elapsed_since_first_frame() { + let mut clock = TimestampClock::new(TimestampMode::Zero); + assert_eq!( + clock.resolve(ts(100, 0)), + DisplayTimestamp::Relative(Duration::ZERO) + ); + assert_eq!( + clock.resolve(ts(100, 250_000_000)), + DisplayTimestamp::Relative(Duration::from_micros(250_000)) + ); + // Relative to the first frame (100), not the previous one. + assert_eq!( + clock.resolve(ts(105, 0)), + DisplayTimestamp::Relative(Duration::from_secs(5)) + ); + } + + #[test] + fn delta_is_gap_between_consecutive_frames() { + let mut clock = TimestampClock::new(TimestampMode::Delta); + assert_eq!( + clock.resolve(ts(100, 0)), + DisplayTimestamp::Relative(Duration::ZERO) + ); + assert_eq!( + clock.resolve(ts(100, 250_000_000)), + DisplayTimestamp::Relative(Duration::from_micros(250_000)) + ); + // Relative to the previous frame (100.25), not the first one. + assert_eq!( + clock.resolve(ts(102, 0)), + DisplayTimestamp::Relative(Duration::from_micros(1_750_000)) + ); + } + + #[test] + fn relative_clamps_backward_clock_to_zero() { + let mut clock = TimestampClock::new(TimestampMode::Delta); + clock.resolve(ts(100, 0)); + assert_eq!( + clock.resolve(ts(99, 0)), + DisplayTimestamp::Relative(Duration::ZERO) + ); + } + + #[test] + fn console_format_standard_data_frame() { + let mut fmt = + CanutilsConsoleFormatter::new(vec!["can0".to_string()], TimestampMode::Absolute); + let mut buf = Vec::new(); + fmt.format( + &frame(0, ts(1, 0), 0x123, &[0xDE, 0xAD, 0xBE, 0xEF]), + &mut buf, + ); + assert_eq!( + String::from_utf8(buf).unwrap(), + "(1.000000) can0 123 [4] DE AD BE EF\n" + ); + } + + #[test] + fn console_format_extended_empty_frame() { + let mut fmt = + CanutilsConsoleFormatter::new(vec!["can0".to_string()], TimestampMode::Absolute); + let mut buf = Vec::new(); + fmt.format( + &frame(0, ts(1, 0), 0x18FECA00 | libc::CAN_EFF_FLAG, &[]), + &mut buf, + ); + assert_eq!( + String::from_utf8(buf).unwrap(), + "(1.000000) can0 18FECA00 [0]\n" + ); + } + + #[test] + fn console_format_error_frame_decodes_to_human_text() { + let mut fmt = + CanutilsConsoleFormatter::new(vec!["can0".to_string()], TimestampMode::Absolute); + let mut buf = Vec::new(); + fmt.format( + &frame(0, ts(1, 0), libc::CAN_ERR_FLAG | 0x40, &[0; 8]), + &mut buf, + ); + assert_eq!( + String::from_utf8(buf).unwrap(), + "(1.000000) can0 20000040 [8] 00 00 00 00 00 00 00 00 ERRORFRAME\n\tbus-off\n" + ); + } + + #[test] + fn console_format_delta_timestamps() { + let mut fmt = CanutilsConsoleFormatter::new(vec!["can0".to_string()], TimestampMode::Delta); + let mut buf = Vec::new(); + fmt.format(&frame(0, ts(100, 0), 0x123, &[0xAB]), &mut buf); + fmt.format(&frame(0, ts(100, 250_000_000), 0x123, &[0xAB]), &mut buf); + assert_eq!( + String::from_utf8(buf).unwrap(), + "(000.000000) can0 123 [1] AB\n(000.250000) can0 123 [1] AB\n" + ); + } } diff --git a/candumpr/src/main.rs b/candumpr/src/main.rs index 1271907..e19af5b 100644 --- a/candumpr/src/main.rs +++ b/candumpr/src/main.rs @@ -5,7 +5,7 @@ use std::time::Duration; use candumpr::can; use candumpr::errframe::{BusState, ErrorFrame}; -use candumpr::format::{CanutilsFormatter, Formatter}; +use candumpr::format::{CanutilsConsoleFormatter, CanutilsFileFormatter, Formatter, TimestampMode}; use candumpr::frame::CanFrame; use candumpr::pipeline::Pipeline; use candumpr::recv::netlink::{self, LinkEvent}; @@ -68,6 +68,15 @@ fn log_error_frames(batch: &[CanFrame], bus_state: &mut [BusState], names: &[Str } } +/// Output format for received frames. +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +enum Format { + /// can-utils candump file format: `(ts) iface ID#DATA`. + CandumpFile, + /// can-utils candump console format: `(ts) iface ID [len] B0 B1 ...`. + CandumpConsole, +} + /// Log CAN traffic from multiple networks. #[derive(Parser)] #[command(version)] @@ -76,6 +85,14 @@ struct Cli { #[arg(required = true)] interfaces: Vec, + /// Output format for received frames. + #[arg(long, value_enum, default_value = "candump-file")] + format: Format, + + /// Timestamp rendering mode. Only applies to the candump formats. + #[arg(long, value_enum, default_value = "absolute")] + timestamp: TimestampMode, + /// Log level for tracing output on stderr. #[arg(long, default_value = "INFO")] log_level: tracing::Level, @@ -157,7 +174,12 @@ fn main() -> ExitCode { // Last logged bus state per sock_id, so we log only transitions. let mut bus_state: Vec = vec![BusState::default(); names.len()]; - let formatter = CanutilsFormatter::new(cli.interfaces); + let formatter: Box = match cli.format { + Format::CandumpFile => Box::new(CanutilsFileFormatter::new(cli.interfaces, cli.timestamp)), + Format::CandumpConsole => { + Box::new(CanutilsConsoleFormatter::new(cli.interfaces, cli.timestamp)) + } + }; let header = formatter.header().map(|h| h.to_vec()); let sink = Sink::new( StdoutWriter::new(), diff --git a/candumpr/src/pipeline.rs b/candumpr/src/pipeline.rs index 3cbdbc3..c7a45ed 100644 --- a/candumpr/src/pipeline.rs +++ b/candumpr/src/pipeline.rs @@ -4,18 +4,18 @@ use crate::recv::Timestamp; use crate::sink::Sink; /// Orchestrates formatting batches of [CanFrame]s and then writing them to each [Sink] -pub struct Pipeline { - formatter: F, +pub struct Pipeline { + formatter: Box, pub(crate) sinks: Vec, bufs: Vec>, first_ts: Vec>, } -impl Pipeline { +impl Pipeline { /// Construct a Pipeline over a non-empty set of sinks. /// /// There should either be one sink, or a sink for every CAN interface being logged. - pub fn new(formatter: F, sinks: Vec) -> Self { + pub fn new(formatter: Box, sinks: Vec) -> Self { assert!(!sinks.is_empty(), "Pipeline requires at least one Sink"); let n = sinks.len(); let bufs = (0..n).map(|_| Vec::with_capacity(4096)).collect(); @@ -109,7 +109,7 @@ mod tests { use super::*; use crate::can::LinuxCanFrame; - use crate::format::CanutilsFormatter; + use crate::format::{CanutilsFileFormatter, TimestampMode}; use crate::frame::Direction; use crate::sink::Sink; use crate::test_util::TestBufWriter; @@ -140,7 +140,7 @@ mod tests { } fn formatted(names: &[String], frames: &[&CanFrame]) -> Vec { - let fmt = CanutilsFormatter::new(names.to_vec()); + let mut fmt = CanutilsFileFormatter::new(names.to_vec(), TimestampMode::Absolute); let mut buf = Vec::new(); for frame in frames { fmt.format(frame, &mut buf); @@ -156,7 +156,13 @@ mod tests { "can2".to_string(), "can3".to_string(), ]; - let mut pipeline = Pipeline::new(CanutilsFormatter::new(names.clone()), vec![sink()]); + let mut pipeline = Pipeline::new( + Box::new(CanutilsFileFormatter::new( + names.clone(), + TimestampMode::Absolute, + )), + vec![sink()], + ); let frames = vec![frame(0, 0x100, &[0x01]), frame(3, 0x200, &[0x02])]; pipeline.write_batch(&frames).unwrap(); @@ -169,7 +175,13 @@ mod tests { fn per_interface_dispatches_by_sock_id() { let names = vec!["can0".to_string(), "can1".to_string(), "can2".to_string()]; let sinks = vec![sink(), sink(), sink()]; - let mut pipeline = Pipeline::new(CanutilsFormatter::new(names.clone()), sinks); + let mut pipeline = Pipeline::new( + Box::new(CanutilsFileFormatter::new( + names.clone(), + TimestampMode::Absolute, + )), + sinks, + ); let frames = vec![ frame(0, 0x100, &[0x0A]), diff --git a/candumpr/src/recv/mod.rs b/candumpr/src/recv/mod.rs index be7ac1e..24ac981 100644 --- a/candumpr/src/recv/mod.rs +++ b/candumpr/src/recv/mod.rs @@ -21,7 +21,7 @@ pub struct FrameMeta { } /// A timestamp with seconds and nanoseconds since the Unix epoch. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Timestamp { pub sec: i64, pub nsec: i64, diff --git a/candumpr/tests/format.rs b/candumpr/tests/format.rs new file mode 100644 index 0000000..d21455c --- /dev/null +++ b/candumpr/tests/format.rs @@ -0,0 +1,55 @@ +use std::os::unix::io::AsFd; +use std::process::{Command, Stdio}; +use std::time::Duration; + +use candumpr::can::{self, LinuxCanFrame}; +use pretty_assertions::assert_eq; +use vcan_fixture::VcanHarness; + +#[ctor::ctor] +fn setup() { + tracing_subscriber::fmt().with_test_writer().init(); + vcan_fixture::enter_namespace(); +} + +fn run_and_log_one_frame(extra_args: &[&str]) -> (String, Vec) { + let vcans = VcanHarness::new(1).unwrap(); + let iface = vcans.names()[0].clone(); + + let mut cmd = Command::new(env!("CARGO_BIN_EXE_candumpr")); + cmd.arg("--log-level=TRACE"); + for arg in extra_args { + cmd.arg(arg); + } + let child = cmd + .arg(&iface) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + + std::thread::sleep(Duration::from_millis(200)); + + let tx = can::open_can_raw_blocking(&iface).unwrap(); + can::send_frame(tx.as_fd(), &LinuxCanFrame::new(0x123, &[0xAB])).unwrap(); + + std::thread::sleep(Duration::from_millis(300)); + unsafe { + libc::kill(child.id() as libc::pid_t, libc::SIGINT); + } + let output = child.wait_with_output().unwrap(); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + let stdout = String::from_utf8(output.stdout).unwrap(); + print!("{stdout}"); + let lines = stdout.lines().map(str::to_string).collect(); + (iface, lines) +} + +#[test] +#[cfg_attr(feature = "ci", ignore = "requires vcan")] +fn console_format_with_zero_timestamp_is_deterministic() { + let (iface, lines) = + run_and_log_one_frame(&["--format", "candump-console", "--timestamp", "zero"]); + assert_eq!(lines.len(), 1); + assert_eq!(lines[0], format!("(000.000000) {iface} 123 [1] AB")); +} diff --git a/docs/design/candumpr/02-architecture.md b/docs/design/candumpr/02-architecture.md index 3ed708c..185acaa 100644 --- a/docs/design/candumpr/02-architecture.md +++ b/docs/design/candumpr/02-architecture.md @@ -246,14 +246,15 @@ rotated due to size or duration limits. ## Formatter detail -The Formatter converts a CanFrame into a byte array to be written. It is stateless (or nearly so) -and is owned by the Pipeline, not by individual Sinks. All configuration (interface names, timestamp -mode, format) is provided at construction time. +The Formatter converts a CanFrame into a byte array to be written. It is owned by the Pipeline, not +by individual Sinks, and all configuration (interface names, timestamp mode, format) is provided at +construction time. Its only mutable state is the relative-timestamp reference used by the delta and +zero timestamp modes, so `format` takes `&mut self`. The Formatter trait has two methods: -* `format(&self, frame: &CanFrame, buf: &mut Vec)` -- append the formatted frame to the buffer. - The buffer may contain multiple frames. A frame is never split across multiple writes. +* `format(&mut self, frame: &CanFrame, buf: &mut Vec)` -- append the formatted frame to the + buffer. The buffer may contain multiple frames. A frame is never split across multiple writes. * `header(&self) -> Option<&[u8]>` -- return the file header for formats that need one (e.g. PCAP), or None for formats that do not (e.g. candump, ASC). The Pipeline provides this header blob to each Sink at construction time, and the Sink writes it when opening a new file.