use std::collections::VecDeque;
use std::time::Duration;
use kcan::{
CAN_TRACE_CSV_HEADER, CanBus, CanFrame, CanFrameClass, CanId, CanMonitor, CanTraceRecord,
ControlMode, Error, FormattedCanFrame, Result, parse_can_trace_csv, replay_can_trace,
};
#[test]
fn monitor_keeps_bounded_rolling_records() {
let mut monitor = CanMonitor::new(2);
monitor.observe(CanFrame::new(CanId::standard(1).unwrap(), &[1]).unwrap());
monitor.observe(CanFrame::new(CanId::standard(2).unwrap(), &[2]).unwrap());
monitor.observe(CanFrame::new(CanId::standard(3).unwrap(), &[3]).unwrap());
let stats = monitor.stats();
assert_eq!(stats.frames_seen, 3);
assert_eq!(stats.frames_retained, 2);
assert_eq!(stats.frames_dropped, 1);
assert_eq!(monitor.records().front().unwrap().sequence, 1);
assert_eq!(monitor.records().back().unwrap().sequence, 2);
}
#[test]
fn classifier_labels_known_protocol_ids() {
let cubemars = CanFrame::new(
ControlMode::VelocityLoop.extended_id(0x03).unwrap(),
&[0, 0, 0, 1],
)
.unwrap();
let robstride = CanFrame::new(CanId::extended(0x501).unwrap(), &[0; 8]).unwrap();
assert_eq!(
CanFrameClass::classify(&cubemars),
CanFrameClass::CubeMarsDirect {
mode: ControlMode::VelocityLoop,
motor_id: 0x03
}
);
assert_eq!(
CanFrameClass::classify(&robstride),
CanFrameClass::RobStrideStatus { motor_id: 1 }
);
}
#[test]
fn formatted_frame_includes_hex_and_ascii_data() {
let frame = CanFrame::new(CanId::standard(0x123).unwrap(), b"A\0z").unwrap();
let formatted = FormattedCanFrame::from_frame(&frame);
assert_eq!(formatted.id, "123");
assert_eq!(formatted.data, "41 00 7A");
assert_eq!(formatted.ascii, "A.z");
assert_eq!(formatted.len, 3);
}
#[test]
fn monitor_polls_available_frames_until_timeout() {
let mut bus = FakeBus::new(vec![
CanFrame::new(CanId::standard(1).unwrap(), &[1]).unwrap(),
CanFrame::new(CanId::extended(0x501).unwrap(), &[2]).unwrap(),
]);
let mut monitor = CanMonitor::new(8);
let received = monitor
.poll_available(&mut bus, Duration::from_millis(1), 8)
.unwrap();
assert_eq!(received, 2);
assert_eq!(monitor.stats().frames_seen, 2);
assert_eq!(monitor.stats().standard_frames, 1);
assert_eq!(monitor.stats().extended_frames, 1);
}
#[test]
fn monitor_exports_and_parses_trace_csv() {
let mut monitor = CanMonitor::new(4);
let standard = CanFrame::new(CanId::standard(0x123).unwrap(), b"kcan").unwrap();
let extended = CanFrame::new(CanId::extended(0x1A_BCDE).unwrap(), &[0xA5, 0x5A]).unwrap();
monitor.observe(standard.clone());
monitor.observe(extended.clone());
let csv = monitor.to_trace_csv();
assert!(csv.starts_with(CAN_TRACE_CSV_HEADER));
assert!(csv.contains(",S,123,4,6B63616E"));
assert!(csv.contains(",E,001ABCDE,2,A55A"));
let trace = parse_can_trace_csv(&csv).unwrap();
assert_eq!(trace.len(), 2);
assert_eq!(trace[0].sequence, 0);
assert_eq!(trace[0].frame, standard);
assert_eq!(trace[1].sequence, 1);
assert_eq!(trace[1].frame, extended);
}
#[test]
fn trace_fixture_parses_expected_protocol_classes() {
let trace = parse_can_trace_csv(include_str!("fixtures/can_trace_protocols.csv")).unwrap();
let classes = trace
.iter()
.map(|record| CanFrameClass::classify(&record.frame))
.collect::<Vec<_>>();
assert_eq!(trace.len(), 4);
assert_eq!(trace[0].sequence, 0);
assert_eq!(trace[1].elapsed, Duration::from_micros(1000));
assert_eq!(trace[3].frame.data(), b"kcan");
assert_eq!(
classes,
vec![
CanFrameClass::CubeMarsDirect {
mode: ControlMode::VelocityLoop,
motor_id: 0x03
},
CanFrameClass::CubeMarsMit { motor_id: 0x02 },
CanFrameClass::RobStrideStatus { motor_id: 0x01 },
CanFrameClass::Standard { id: 0x123 }
]
);
}
#[test]
fn trace_csv_parser_rejects_bad_payload_length() {
let err =
parse_can_trace_csv("sequence,elapsed_us,frame_format,id,len,data_hex\n0,0,S,123,2,AA\n")
.unwrap_err();
assert!(
err.to_string()
.contains("data_hex length 2 does not match len 2")
);
}
#[test]
fn trace_csv_parser_rejects_invalid_standard_id() {
let err =
parse_can_trace_csv("sequence,elapsed_us,frame_format,id,len,data_hex\n0,0,S,800,0,\n")
.unwrap_err();
assert!(
err.to_string()
.contains("standard id 0x800 exceeds 11-bit range")
);
}
#[test]
fn trace_replay_sends_frames_in_order() {
let first = CanFrame::new(CanId::standard(0x100).unwrap(), &[1]).unwrap();
let second = CanFrame::new(CanId::extended(0x400).unwrap(), &[2, 3]).unwrap();
let trace = vec![
CanTraceRecord {
sequence: 4,
elapsed: Duration::from_micros(10),
frame: first.clone(),
},
CanTraceRecord {
sequence: 5,
elapsed: Duration::from_micros(20),
frame: second.clone(),
},
];
let mut bus = RecordingBus::default();
let sent = replay_can_trace(&mut bus, &trace).unwrap();
assert_eq!(sent, 2);
assert_eq!(bus.sent, vec![first, second]);
}
struct FakeBus {
frames: VecDeque<CanFrame>,
}
#[derive(Default)]
struct RecordingBus {
sent: Vec<CanFrame>,
}
impl FakeBus {
fn new(frames: Vec<CanFrame>) -> Self {
Self {
frames: frames.into(),
}
}
}
impl CanBus for FakeBus {
fn send(&mut self, _frame: &CanFrame) -> Result<()> {
Err(Error::Transport("not implemented".to_owned()))
}
fn receive(&mut self, _timeout: Duration) -> Result<Option<CanFrame>> {
Ok(self.frames.pop_front())
}
}
impl CanBus for RecordingBus {
fn send(&mut self, frame: &CanFrame) -> Result<()> {
self.sent.push(frame.clone());
Ok(())
}
fn receive(&mut self, _timeout: Duration) -> Result<Option<CanFrame>> {
Ok(None)
}
}