use super::*;
fn make_point(x: f32, y: f32) -> LaserPoint {
LaserPoint::new(x, y, 65535, 0, 0, 65535)
}
#[test]
fn test_authored_frame_new_and_points() {
let pts = vec![make_point(0.0, 0.0), make_point(1.0, 1.0)];
let frame = Frame::new(pts.clone());
assert_eq!(frame.points().len(), 2);
assert_eq!(frame.points()[0].x, 0.0);
assert_eq!(frame.points()[1].x, 1.0);
}
#[test]
fn test_authored_frame_first_last_point() {
let frame = Frame::new(vec![
make_point(-1.0, -1.0),
make_point(0.0, 0.0),
make_point(1.0, 1.0),
]);
assert_eq!(frame.first_point().unwrap().x, -1.0);
assert_eq!(frame.last_point().unwrap().x, 1.0);
}
#[test]
fn test_authored_frame_empty() {
let frame = Frame::new(vec![]);
assert!(frame.is_empty());
assert_eq!(frame.len(), 0);
assert!(frame.first_point().is_none());
assert!(frame.last_point().is_none());
}
#[test]
fn test_authored_frame_len() {
let frame = Frame::new(vec![make_point(0.0, 0.0); 42]);
assert_eq!(frame.len(), 42);
assert!(!frame.is_empty());
}
#[test]
fn test_authored_frame_from_vec() {
let pts = vec![make_point(0.5, 0.5)];
let frame: Frame = pts.into();
assert_eq!(frame.len(), 1);
assert_eq!(frame.points()[0].x, 0.5);
}
#[test]
fn test_authored_frame_clone_shares_data() {
let frame = Frame::new(vec![make_point(0.0, 0.0)]);
let clone = frame.clone();
assert_eq!(frame.len(), clone.len());
assert_eq!(frame.points()[0].x, clone.points()[0].x);
}
fn call_default_transition(from: &LaserPoint, to: &LaserPoint) -> TransitionPlan {
let tf = default_transition(30_000);
tf(from, to)
}
#[test]
fn test_default_transition_scales_with_distance() {
let near = call_default_transition(&make_point(0.0, 0.0), &make_point(0.005, 0.0));
let far = call_default_transition(&make_point(-1.0, -1.0), &make_point(1.0, 1.0));
let mid = call_default_transition(&make_point(0.0, 0.0), &make_point(1.0, 0.0));
let near_pts = match near {
TransitionPlan::Transition(pts) => pts,
_ => panic!("near should produce Transition"),
};
let far_pts = match far {
TransitionPlan::Transition(pts) => pts,
_ => panic!("far should produce Transition"),
};
let mid_pts = match mid {
TransitionPlan::Transition(pts) => pts,
_ => panic!("mid should produce Transition"),
};
assert!(
near_pts.len() >= 3,
"near should still produce blanking, got {}",
near_pts.len()
);
assert!(
far_pts.len() > mid_pts.len(),
"far should produce more points than medium"
);
assert!(
mid_pts.len() > near_pts.len(),
"medium should produce more points than near"
);
}
#[test]
fn test_default_transition_always_blanks() {
let result = call_default_transition(&make_point(0.0, 0.0), &make_point(0.01, 0.0));
let pts = match result {
TransitionPlan::Transition(pts) => pts,
_ => panic!("should produce Transition"),
};
assert!(
pts.len() >= 3,
"even tiny distance should produce blanking, got {}",
pts.len()
);
let result = call_default_transition(&make_point(0.0, 0.0), &make_point(1.0, 0.0));
let pts2 = match result {
TransitionPlan::Transition(pts) => pts,
_ => panic!("should produce Transition"),
};
assert!(pts2.len() > pts.len());
}
#[test]
fn test_default_transition_all_blanked() {
let from = make_point(0.0, 0.0);
let to = make_point(1.0, 1.0);
let result = match call_default_transition(&from, &to) {
TransitionPlan::Transition(pts) => pts,
_ => panic!("expected Transition"),
};
for p in &result {
assert_eq!(p.r, 0, "r should be 0");
assert_eq!(p.g, 0, "g should be 0");
assert_eq!(p.b, 0, "b should be 0");
assert_eq!(p.intensity, 0, "intensity should be 0");
}
}
#[test]
fn test_default_transition_three_phase_structure() {
let from = make_point(0.0, 0.0);
let to = make_point(1.0, 0.0);
let result = match call_default_transition(&from, &to) {
TransitionPlan::Transition(pts) => pts,
_ => panic!("expected Transition"),
};
assert_eq!(
result.len(),
47,
"3 end_dwell + 32 transit + 12 start_dwell"
);
for p in &result[..3] {
assert_eq!(p.x, 0.0, "end dwell should be at from.x");
assert_eq!(p.intensity, 0);
}
for p in &result[3..35] {
assert!(
p.x > 0.0 && p.x < 1.0,
"transit should be between from and to"
);
assert_eq!(p.intensity, 0);
}
for p in &result[35..] {
assert_eq!(p.x, 1.0, "start dwell should be at to.x");
assert_eq!(p.intensity, 0);
}
}
#[test]
fn test_default_transition_full_diagonal() {
let from = make_point(-1.0, -1.0);
let to = make_point(1.0, 1.0);
let result = match call_default_transition(&from, &to) {
TransitionPlan::Transition(pts) => pts,
_ => panic!("expected Transition"),
};
assert_eq!(result.len(), 79, "3 end + 64 transit + 12 start");
}
#[test]
fn test_default_transition_same_point() {
let p = make_point(0.5, -0.3);
let result = match call_default_transition(&p, &p) {
TransitionPlan::Transition(pts) => pts,
_ => panic!("expected Transition"),
};
assert_eq!(result.len(), 15, "end_dwell + start_dwell, no transit");
for pt in &result {
assert_eq!(pt.intensity, 0);
assert_eq!(pt.x, 0.5);
assert_eq!(pt.y, -0.3);
}
}
#[test]
fn test_default_transition_quintic_easing() {
let from = make_point(0.0, 0.0);
let to = make_point(1.0, 0.0);
let result = match call_default_transition(&from, &to) {
TransitionPlan::Transition(pts) => pts,
_ => panic!("expected Transition"),
};
let transit = &result[3..35];
assert!(
transit[0].x < 0.25,
"first transit point should be near from, got {}",
transit[0].x
);
assert!(
transit[31].x > 0.75,
"last transit point should be near to, got {}",
transit[31].x
);
}
#[test]
fn test_default_transition_pps_scales_dwells() {
let tf_low = default_transition(10_000);
let tf_high = default_transition(100_000);
let from = make_point(0.0, 0.0);
let to = make_point(0.0, 0.0);
let low_pts = match tf_low(&from, &to) {
TransitionPlan::Transition(pts) => pts,
_ => panic!("expected Transition"),
};
let high_pts = match tf_high(&from, &to) {
TransitionPlan::Transition(pts) => pts,
_ => panic!("expected Transition"),
};
assert!(
high_pts.len() > low_pts.len(),
"higher PPS should produce more dwell points: {} vs {}",
high_pts.len(),
low_pts.len()
);
}
fn make_engine() -> PresentationEngine {
PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![
LaserPoint::blanked(from.x * 0.5 + to.x * 0.5, from.y * 0.5 + to.y * 0.5),
LaserPoint::blanked(to.x, to.y),
])
}))
}
fn make_engine_no_transition() -> PresentationEngine {
PresentationEngine::new(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}))
}
fn make_engine_coalesce() -> PresentationEngine {
PresentationEngine::new(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Coalesce
}))
}
fn make_frame(points: Vec<LaserPoint>) -> Frame {
Frame::new(points)
}
#[test]
fn test_engine_before_first_frame_blanks_at_origin() {
let mut engine = make_engine();
let mut buffer = vec![LaserPoint::default(); 10];
let n = engine.fill_chunk(&mut buffer, 10);
assert_eq!(n, 10);
for p in &buffer {
assert_eq!(p.x, 0.0);
assert_eq!(p.y, 0.0);
assert_eq!(p.intensity, 0);
}
}
#[test]
fn test_engine_set_pending_promotes_when_no_current() {
let mut engine = make_engine_no_transition();
let frame = make_frame(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]);
engine.set_pending(frame);
assert!(engine.current_base.is_some());
assert!(engine.pending_base.is_none());
}
#[test]
fn test_engine_set_pending_overwrites_existing_pending() {
let mut engine = make_engine_no_transition();
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
let frame_b = make_frame(vec![make_point(2.0, 0.0)]);
let frame_c = make_frame(vec![make_point(3.0, 0.0)]);
engine.set_pending(frame_a); engine.set_pending(frame_b); engine.set_pending(frame_c);
assert!(engine.pending_base.is_some());
assert_eq!(engine.pending_base.as_ref().unwrap().points()[0].x, 3.0);
}
#[test]
fn test_engine_fill_chunk_cycles_frame() {
let mut engine = make_engine_no_transition();
let frame = make_frame(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]);
engine.set_pending(frame);
let mut buffer = vec![LaserPoint::default(); 6];
let n = engine.fill_chunk(&mut buffer, 6);
assert_eq!(n, 6);
assert_eq!(buffer[0].x, 1.0);
assert_eq!(buffer[1].x, 2.0);
assert_eq!(buffer[2].x, 1.0);
assert_eq!(buffer[3].x, 2.0);
}
#[test]
fn test_engine_fill_chunk_self_loop_with_transition() {
let mut engine = make_engine(); let frame = make_frame(vec![make_point(0.0, 0.0), make_point(1.0, 0.0)]);
engine.set_pending(frame);
let mut buffer = vec![LaserPoint::default(); 8];
let n = engine.fill_chunk(&mut buffer, 8);
assert_eq!(n, 8);
assert_eq!(buffer[0].x, 0.0); assert_eq!(buffer[1].x, 1.0); assert_eq!(buffer[2].intensity, 0);
assert_eq!(buffer[3].intensity, 0);
assert_eq!(buffer[4].x, 0.0);
assert_eq!(buffer[5].x, 1.0);
}
#[test]
fn test_engine_fill_chunk_frame_change_inserts_transition() {
let mut engine = make_engine(); let frame_a = make_frame(vec![make_point(0.0, 0.0)]);
let frame_b = make_frame(vec![make_point(1.0, 0.0)]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
let mut buffer = vec![LaserPoint::default(); 6];
let n = engine.fill_chunk(&mut buffer, 6);
assert_eq!(n, 6);
assert_eq!(buffer[0].x, 0.0);
assert_eq!(buffer[0].intensity, 65535);
assert_eq!(buffer[1].intensity, 0);
assert_eq!(buffer[2].intensity, 0);
assert_eq!(buffer[3].x, 1.0);
assert_eq!(buffer[3].intensity, 65535);
assert_eq!(buffer[4].intensity, 0);
assert_eq!(buffer[5].intensity, 0);
}
#[test]
fn test_engine_fill_chunk_promotes_pending_at_frame_end() {
let mut engine = make_engine_no_transition();
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
let frame_b = make_frame(vec![make_point(2.0, 0.0)]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
let mut buffer = vec![LaserPoint::default(); 4];
let n = engine.fill_chunk(&mut buffer, 4);
assert_eq!(n, 4);
assert_eq!(buffer[0].x, 1.0); assert_eq!(buffer[1].x, 2.0); assert_eq!(buffer[2].x, 2.0); }
#[test]
fn test_engine_fill_chunk_frame_skip() {
let mut engine = make_engine_no_transition();
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
let frame_b = make_frame(vec![make_point(2.0, 0.0)]);
let frame_c = make_frame(vec![make_point(3.0, 0.0)]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
engine.set_pending(frame_c);
let mut buffer = vec![LaserPoint::default(); 4];
engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 1.0);
assert_eq!(buffer[1].x, 3.0);
}
#[test]
fn test_engine_fill_chunk_cursor_continuity() {
let mut engine = make_engine_no_transition();
let frame = make_frame(vec![
make_point(0.0, 0.0),
make_point(1.0, 0.0),
make_point(2.0, 0.0),
]);
engine.set_pending(frame);
let mut buf1 = vec![LaserPoint::default(); 2];
engine.fill_chunk(&mut buf1, 2);
assert_eq!(buf1[0].x, 0.0);
assert_eq!(buf1[1].x, 1.0);
let mut buf2 = vec![LaserPoint::default(); 2];
engine.fill_chunk(&mut buf2, 2);
assert_eq!(buf2[0].x, 2.0);
assert_eq!(buf2[1].x, 0.0);
}
#[test]
fn test_engine_compose_hardware_frame_self_loop() {
let mut engine = make_engine(); let frame = make_frame(vec![make_point(0.0, 0.0), make_point(1.0, 0.0)]);
engine.set_pending(frame);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 4);
assert_eq!(composed[0].intensity, 0);
assert_eq!(composed[1].intensity, 0);
assert_eq!(composed[2].x, 0.0);
assert_eq!(composed[3].x, 1.0);
}
#[test]
fn test_engine_compose_hardware_frame_promotes_pending() {
let mut engine = make_engine_no_transition();
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
let frame_b = make_frame(vec![make_point(2.0, 0.0)]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 1);
assert_eq!(composed[0].x, 2.0);
}
#[test]
fn test_engine_compose_hardware_frame_empty_before_first_frame() {
let mut engine = make_engine();
let composed = engine.compose_hardware_frame();
assert!(composed.is_empty());
}
#[test]
fn test_engine_compose_hardware_frame_a_to_b_transition() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
}));
let frame_a = make_frame(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]);
engine.set_pending(frame_a);
let _ = engine.compose_hardware_frame();
let frame_b = make_frame(vec![make_point(5.0, 0.0), make_point(6.0, 0.0)]);
engine.set_pending(frame_b);
let composed = engine.compose_hardware_frame();
assert_eq!(composed[0].x, 2.0, "transition 'from' should be A.last");
assert_eq!(composed[0].y, 5.0, "transition 'to' should be B.first");
assert_eq!(composed[1].x, 5.0);
assert_eq!(composed[2].x, 6.0);
}
#[test]
fn test_engine_compose_hardware_frame_self_loop_after_transition() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
}));
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
engine.set_pending(frame_a);
let _ = engine.compose_hardware_frame();
let frame_b = make_frame(vec![make_point(5.0, 0.0), make_point(6.0, 0.0)]);
engine.set_pending(frame_b);
let _ = engine.compose_hardware_frame();
let composed = engine.compose_hardware_frame();
assert_eq!(composed[0].x, 6.0, "self-loop 'from' should be B.last");
assert_eq!(composed[0].y, 5.0, "self-loop 'to' should be B.first");
assert_eq!(composed[1].x, 5.0);
assert_eq!(composed[2].x, 6.0);
}
#[test]
fn test_engine_compose_hardware_frame_skip() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
}));
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
engine.set_pending(frame_a);
let _ = engine.compose_hardware_frame();
let frame_b = make_frame(vec![make_point(5.0, 0.0)]);
engine.set_pending(frame_b);
let frame_c = make_frame(vec![make_point(9.0, 0.0)]);
engine.set_pending(frame_c);
let composed = engine.compose_hardware_frame();
assert_eq!(composed[0].x, 1.0, "transition 'from' should be A.last");
assert_eq!(
composed[0].y, 9.0,
"transition 'to' should be C.first (B skipped)"
);
assert_eq!(composed[1].x, 9.0);
}
#[test]
fn test_engine_compose_hardware_frame_empty_frame_produces_blanked_point() {
let mut engine = make_engine_no_transition();
let empty = make_frame(vec![]);
engine.set_pending(empty);
let composed = engine.compose_hardware_frame();
assert_eq!(
composed.len(),
1,
"empty frame should produce exactly 1 blanked point"
);
assert_eq!(composed[0].x, 0.0);
assert_eq!(composed[0].y, 0.0);
assert_eq!(composed[0].intensity, 0, "point should be blanked");
}
#[test]
fn test_engine_compose_hardware_frame_transition_to_empty_produces_blanked_point() {
let mut engine = make_engine_no_transition();
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
engine.set_pending(frame_a);
let _ = engine.compose_hardware_frame();
let empty = make_frame(vec![]);
engine.set_pending(empty);
let composed = engine.compose_hardware_frame();
assert!(
!composed.is_empty(),
"A→empty transition should not produce empty drawable"
);
for p in composed {
assert_eq!(p.intensity, 0, "all points should be blanked");
}
}
#[test]
fn test_engine_compose_hardware_frame_self_loop_empty_produces_blanked_point() {
let mut engine = make_engine_no_transition();
let empty = make_frame(vec![]);
engine.set_pending(empty);
let _ = engine.compose_hardware_frame();
let composed = engine.compose_hardware_frame();
assert_eq!(
composed.len(),
1,
"self-loop on empty should produce 1 blanked point"
);
assert_eq!(composed[0].intensity, 0);
}
#[test]
fn test_engine_compose_hardware_frame_clamps_to_capacity() {
let mut engine = PresentationEngine::new(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(0.0, 0.0); 10])
}));
engine.set_frame_capacity(Some(8));
let frame = make_frame(vec![
make_point(1.0, 0.0),
make_point(2.0, 0.0),
make_point(3.0, 0.0),
make_point(4.0, 0.0),
make_point(5.0, 0.0),
]);
engine.set_pending(frame);
let composed = engine.compose_hardware_frame();
assert!(
composed.len() <= 8,
"composed frame should not exceed capacity, got {}",
composed.len()
);
let frame_points: Vec<f32> = composed
.iter()
.filter(|p| p.intensity > 0)
.map(|p| p.x)
.collect();
assert_eq!(
frame_points,
vec![1.0, 2.0, 3.0, 4.0, 5.0],
"authored points must be preserved"
);
}
#[test]
fn test_engine_compose_hardware_frame_no_truncation_under_capacity() {
let mut engine = PresentationEngine::new(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(0.0, 0.0); 3])
}));
engine.set_frame_capacity(Some(100));
let frame = make_frame(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]);
engine.set_pending(frame);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 5, "no truncation expected");
}
#[test]
fn test_engine_compose_hardware_frame_self_loop_clamps_to_capacity() {
let mut engine = PresentationEngine::new(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(0.0, 0.0); 20])
}));
engine.set_frame_capacity(Some(6));
let frame = make_frame(vec![make_point(0.0, 0.0), make_point(1.0, 0.0)]);
engine.set_pending(frame);
let _ = engine.compose_hardware_frame();
let composed = engine.compose_hardware_frame();
assert!(
composed.len() <= 6,
"self-loop should clamp to capacity, got {}",
composed.len()
);
}
#[test]
fn test_engine_fill_chunk_multiple_wraps_in_single_call() {
let mut engine = make_engine_no_transition();
let frame = make_frame(vec![make_point(5.0, 0.0)]);
engine.set_pending(frame);
let mut buffer = vec![LaserPoint::default(); 10];
let n = engine.fill_chunk(&mut buffer, 10);
assert_eq!(n, 10);
for p in &buffer {
assert_eq!(p.x, 5.0);
}
}
#[test]
fn test_fifo_no_stale_self_loop_seam() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
}));
let frame_a = make_frame(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]);
let frame_b = make_frame(vec![make_point(5.0, 0.0)]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
let mut buffer = vec![LaserPoint::default(); 4];
engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 1.0);
assert_eq!(buffer[0].intensity, 65535);
assert_eq!(buffer[1].x, 2.0);
assert_eq!(buffer[1].intensity, 65535);
assert_eq!(buffer[2].x, 2.0, "transition 'from' should be A.last");
assert_eq!(
buffer[2].y, 5.0,
"transition 'to' should be B.first, not A.first"
);
assert_eq!(buffer[2].intensity, 0);
assert_eq!(buffer[3].x, 5.0);
assert_eq!(buffer[3].intensity, 65535);
}
#[test]
fn test_fifo_pending_at_seam_uses_latest() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
}));
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
let frame_b = make_frame(vec![make_point(5.0, 0.0)]);
let frame_c = make_frame(vec![make_point(9.0, 0.0)]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
engine.set_pending(frame_c);
let mut buffer = vec![LaserPoint::default(); 4];
engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 1.0);
assert_eq!(buffer[1].x, 1.0, "transition 'from' = A.last");
assert_eq!(buffer[1].y, 9.0, "transition 'to' = C.first, B skipped");
assert_eq!(buffer[1].intensity, 0);
assert_eq!(buffer[2].x, 9.0);
assert_eq!(buffer[2].intensity, 65535);
}
#[test]
fn test_fifo_stale_self_loop_discarded_across_chunks() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![
LaserPoint::blanked(from.x, to.x),
LaserPoint::blanked(to.x, to.y),
])
}));
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
engine.set_pending(frame_a);
let mut buf1 = vec![LaserPoint::default(); 1];
engine.fill_chunk(&mut buf1, 1);
assert_eq!(buf1[0].x, 1.0);
let frame_b = make_frame(vec![make_point(5.0, 0.0)]);
engine.set_pending(frame_b);
let mut buf2 = vec![LaserPoint::default(); 4];
engine.fill_chunk(&mut buf2, 4);
assert_eq!(
buf2[0].intensity, 0,
"should be A→B transition, not stale A→A"
);
assert_eq!(buf2[1].intensity, 0);
assert_eq!(buf2[2].x, 5.0);
assert_eq!(buf2[2].intensity, 65535);
}
#[test]
fn test_fifo_self_loop_coalesce_omits_last_point() {
let mut engine = make_engine_coalesce();
let frame = make_frame(vec![
make_point(0.0, 0.0),
make_point(1.0, 0.0),
make_point(0.0, 0.0), ]);
engine.set_pending(frame);
let mut buffer = vec![LaserPoint::default(); 6];
let n = engine.fill_chunk(&mut buffer, 6);
assert_eq!(n, 6);
assert_eq!(buffer[0].x, 0.0);
assert_eq!(buffer[1].x, 1.0);
assert_eq!(buffer[2].x, 0.0); assert_eq!(buffer[3].x, 1.0);
for p in &buffer[..6] {
assert_eq!(p.intensity, 65535);
}
}
#[test]
fn test_fifo_self_loop_coalesce_single_point_preserved() {
let mut engine = make_engine_coalesce();
let frame = make_frame(vec![make_point(5.0, 0.0)]);
engine.set_pending(frame);
let mut buffer = vec![LaserPoint::default(); 4];
let n = engine.fill_chunk(&mut buffer, 4);
assert_eq!(n, 4);
for p in &buffer {
assert_eq!(p.x, 5.0);
assert_eq!(p.intensity, 65535);
}
}
#[test]
fn test_fifo_a_to_b_coalesce_skips_incoming_first() {
let mut engine = make_engine_coalesce();
let frame_a = make_frame(vec![make_point(0.0, 0.0)]);
let frame_b = make_frame(vec![
make_point(0.0, 0.0), make_point(1.0, 0.0),
make_point(2.0, 0.0),
]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
let mut buffer = vec![LaserPoint::default(); 6];
engine.fill_chunk(&mut buffer, 6);
assert_eq!(buffer[0].x, 0.0); assert_eq!(buffer[1].x, 1.0); }
#[test]
fn test_fifo_a_to_c_skip_latest_coalesce() {
let mut engine = make_engine_coalesce();
let frame_a = make_frame(vec![make_point(0.0, 0.0)]);
let frame_b = make_frame(vec![make_point(5.0, 0.0)]); let frame_c = make_frame(vec![
make_point(0.0, 0.0), make_point(3.0, 0.0),
]);
engine.set_pending(frame_a);
engine.set_pending(frame_b);
engine.set_pending(frame_c);
let mut buffer = vec![LaserPoint::default(); 4];
engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 0.0);
assert_eq!(buffer[1].x, 3.0);
assert_eq!(buffer[2].x, 3.0);
assert_eq!(buffer[3].x, 3.0);
}
#[test]
fn test_frame_swap_self_loop_coalesce() {
let mut engine = make_engine_coalesce();
let frame = make_frame(vec![
make_point(0.0, 0.0),
make_point(1.0, 0.0),
make_point(0.0, 0.0), ]);
engine.set_pending(frame);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 2, "coalesce should omit last point");
assert_eq!(composed[0].x, 0.0);
assert_eq!(composed[1].x, 1.0);
assert_eq!(composed[0].intensity, 65535);
assert_eq!(composed[1].intensity, 65535);
}
#[test]
fn test_frame_swap_a_to_b_coalesce() {
let mut engine = make_engine_coalesce();
let frame_a = make_frame(vec![make_point(0.0, 0.0)]);
engine.set_pending(frame_a);
let _ = engine.compose_hardware_frame();
let frame_b = make_frame(vec![make_point(0.0, 0.0), make_point(1.0, 0.0)]);
engine.set_pending(frame_b);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 1);
assert_eq!(composed[0].x, 1.0);
assert_eq!(composed[0].intensity, 65535);
}
#[test]
fn test_parity_self_loop_with_transition() {
let transition_fn = || -> TransitionFn {
Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![
LaserPoint::blanked(from.x * 0.5 + to.x * 0.5, 0.0),
LaserPoint::blanked(to.x, to.y),
])
})
};
let frame = make_frame(vec![make_point(0.0, 0.0), make_point(1.0, 0.0)]);
let mut fifo_engine = PresentationEngine::new(transition_fn());
fifo_engine.set_pending(frame.clone());
let mut buffer = vec![LaserPoint::default(); 8];
fifo_engine.fill_chunk(&mut buffer, 8);
let fifo_intensities: Vec<u16> = buffer.iter().map(|p| p.intensity).collect();
assert_eq!(
fifo_intensities,
vec![65535, 65535, 0, 0, 65535, 65535, 0, 0],
"FIFO self-loop should include transition"
);
let mut swap_engine = PresentationEngine::new(transition_fn());
swap_engine.set_pending(frame);
let composed = swap_engine.compose_hardware_frame();
assert_eq!(composed.len(), 4, "FrameSwap self-loop: 2 trans + 2 frame");
assert_eq!(composed[0].intensity, 0); assert_eq!(composed[1].intensity, 0);
assert_eq!(composed[2].intensity, 65535); assert_eq!(composed[3].intensity, 65535);
}
#[test]
fn test_parity_self_loop_with_coalesce() {
let frame = make_frame(vec![
make_point(0.0, 0.0),
make_point(1.0, 0.0),
make_point(2.0, 0.0),
]);
let mut fifo_engine = make_engine_coalesce();
fifo_engine.set_pending(frame.clone());
let mut buffer = vec![LaserPoint::default(); 6];
fifo_engine.fill_chunk(&mut buffer, 6);
assert_eq!(buffer[0].x, 0.0);
assert_eq!(buffer[1].x, 1.0);
assert_eq!(buffer[2].x, 2.0);
assert_eq!(buffer[3].x, 1.0);
assert_eq!(buffer[4].x, 2.0);
assert_eq!(buffer[5].x, 1.0);
let mut swap_engine = make_engine_coalesce();
swap_engine.set_pending(frame);
let composed = swap_engine.compose_hardware_frame();
assert_eq!(composed.len(), 2);
assert_eq!(composed[0].x, 0.0);
assert_eq!(composed[1].x, 1.0);
}
#[test]
fn test_parity_a_to_b_with_transition() {
let transition_fn = || -> TransitionFn {
Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
})
};
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
let frame_b = make_frame(vec![make_point(5.0, 0.0)]);
let mut fifo_engine = PresentationEngine::new(transition_fn());
fifo_engine.set_pending(frame_a.clone());
fifo_engine.set_pending(frame_b.clone());
let mut buffer = vec![LaserPoint::default(); 4];
fifo_engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 1.0);
assert_eq!(buffer[0].intensity, 65535);
assert_eq!(buffer[1].intensity, 0); assert_eq!(buffer[2].x, 5.0);
assert_eq!(buffer[2].intensity, 65535);
assert_eq!(buffer[3].intensity, 0);
let mut swap_engine = PresentationEngine::new(transition_fn());
swap_engine.set_pending(frame_a);
let _ = swap_engine.compose_hardware_frame(); swap_engine.set_pending(frame_b);
let composed = swap_engine.compose_hardware_frame();
assert_eq!(composed[0].x, 1.0);
assert_eq!(composed[0].intensity, 0);
assert_eq!(composed[1].x, 5.0);
assert_eq!(composed[1].intensity, 65535);
}
#[test]
fn test_parity_a_to_b_with_coalesce() {
let frame_a = make_frame(vec![make_point(0.0, 0.0)]);
let frame_b = make_frame(vec![
make_point(0.0, 0.0), make_point(1.0, 0.0),
make_point(2.0, 0.0),
]);
let mut fifo_engine = make_engine_coalesce();
fifo_engine.set_pending(frame_a.clone());
fifo_engine.set_pending(frame_b.clone());
let mut buffer = vec![LaserPoint::default(); 4];
fifo_engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 0.0); assert_eq!(buffer[1].x, 1.0);
let mut swap_engine = make_engine_coalesce();
swap_engine.set_pending(frame_a);
let _ = swap_engine.compose_hardware_frame(); swap_engine.set_pending(frame_b);
let composed = swap_engine.compose_hardware_frame();
assert_eq!(composed.len(), 2);
assert_eq!(composed[0].x, 1.0);
assert_eq!(composed[1].x, 2.0);
}
#[test]
fn test_parity_a_to_c_skip() {
let transition_fn = || -> TransitionFn {
Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
})
};
let frame_a = make_frame(vec![make_point(1.0, 0.0)]);
let frame_b = make_frame(vec![make_point(5.0, 0.0)]); let frame_c = make_frame(vec![make_point(9.0, 0.0)]);
let mut fifo_engine = PresentationEngine::new(transition_fn());
fifo_engine.set_pending(frame_a.clone());
fifo_engine.set_pending(frame_b.clone());
fifo_engine.set_pending(frame_c.clone()); let mut buffer = vec![LaserPoint::default(); 4];
fifo_engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 1.0);
assert_eq!(buffer[1].intensity, 0); assert_eq!(buffer[2].x, 9.0);
assert_eq!(buffer[3].intensity, 0);
let mut swap_engine = PresentationEngine::new(transition_fn());
swap_engine.set_pending(frame_a);
let _ = swap_engine.compose_hardware_frame();
swap_engine.set_pending(frame_b);
swap_engine.set_pending(frame_c); let composed = swap_engine.compose_hardware_frame();
assert_eq!(composed[0].x, 1.0);
assert_eq!(composed[0].intensity, 0);
assert_eq!(composed[1].x, 9.0);
}
use crate::backend::{BackendKind, DacBackend, FifoBackend, FrameSwapBackend};
use crate::error::Result as DacResult;
use crate::stream::RunExit;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Mutex;
#[derive(Debug, Clone)]
struct RecordedFilterCall {
points: Vec<LaserPoint>,
ctx: OutputFilterContext,
}
#[derive(Default)]
struct FilterObservations {
calls: AtomicUsize,
resets: Mutex<Vec<OutputResetReason>>,
invocations: Mutex<Vec<RecordedFilterCall>>,
}
struct RecordingFilter {
observations: Arc<FilterObservations>,
intensity_tag: Option<u16>,
}
impl RecordingFilter {
fn new() -> (Self, Arc<FilterObservations>) {
let observations = Arc::new(FilterObservations::default());
(
Self {
observations: observations.clone(),
intensity_tag: None,
},
observations,
)
}
fn with_intensity_tag(tag: u16) -> (Self, Arc<FilterObservations>) {
let observations = Arc::new(FilterObservations::default());
(
Self {
observations: observations.clone(),
intensity_tag: Some(tag),
},
observations,
)
}
}
impl OutputFilter for RecordingFilter {
fn reset(&mut self, reason: OutputResetReason) {
self.observations.resets.lock().unwrap().push(reason);
}
fn filter(&mut self, points: &mut [LaserPoint], ctx: &OutputFilterContext) {
if let Some(tag) = self.intensity_tag {
for point in points.iter_mut() {
point.intensity = tag;
}
}
self.observations.calls.fetch_add(1, Ordering::SeqCst);
self.observations
.invocations
.lock()
.unwrap()
.push(RecordedFilterCall {
points: points.to_vec(),
ctx: *ctx,
});
}
}
struct StampingFilter {
observations: Arc<FilterObservations>,
next_stamp: u16,
}
impl StampingFilter {
fn new() -> (Self, Arc<FilterObservations>) {
let observations = Arc::new(FilterObservations::default());
(
Self {
observations: observations.clone(),
next_stamp: 1,
},
observations,
)
}
}
impl OutputFilter for StampingFilter {
fn reset(&mut self, reason: OutputResetReason) {
self.observations.resets.lock().unwrap().push(reason);
}
fn filter(&mut self, points: &mut [LaserPoint], ctx: &OutputFilterContext) {
let stamp = self.next_stamp;
self.next_stamp = self.next_stamp.saturating_add(1);
for point in points.iter_mut() {
point.intensity = stamp;
}
self.observations.calls.fetch_add(1, Ordering::SeqCst);
self.observations
.invocations
.lock()
.unwrap()
.push(RecordedFilterCall {
points: points.to_vec(),
ctx: *ctx,
});
}
}
fn wait_for_filter_calls(
observations: &Arc<FilterObservations>,
min_len: usize,
timeout: std::time::Duration,
) {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if observations.calls.load(Ordering::SeqCst) >= min_len {
return;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!(
"timed out waiting for {} filter calls, got {}",
min_len,
observations.calls.load(Ordering::SeqCst)
);
}
fn wait_for_filter_reset(
observations: &Arc<FilterObservations>,
reason: OutputResetReason,
timeout: std::time::Duration,
) {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if observations
.resets
.lock()
.unwrap()
.iter()
.copied()
.any(|reset| reset == reason)
{
return;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!("timed out waiting for reset {reason:?}");
}
struct FifoTestBackend {
connected: bool,
write_count: Arc<AtomicUsize>,
points_written: Arc<AtomicUsize>,
shutter_open: Arc<AtomicBool>,
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
}
impl FifoTestBackend {
fn new() -> Self {
Self {
connected: false,
write_count: Arc::new(AtomicUsize::new(0)),
points_written: Arc::new(AtomicUsize::new(0)),
shutter_open: Arc::new(AtomicBool::new(false)),
writes: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl DacBackend for FifoTestBackend {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("FifoTest".into())
}
fn caps(&self) -> &crate::device::DacCapabilities {
static CAPS: crate::device::DacCapabilities = crate::device::DacCapabilities {
pps_min: 1000,
pps_max: 100000,
max_points_per_chunk: 1000,
output_model: crate::device::OutputModel::NetworkFifo,
};
&CAPS
}
fn connect(&mut self) -> DacResult<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> DacResult<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> DacResult<()> {
Ok(())
}
fn set_shutter(&mut self, open: bool) -> DacResult<()> {
self.shutter_open.store(open, Ordering::SeqCst);
Ok(())
}
}
impl FifoBackend for FifoTestBackend {
fn try_write_points(
&mut self,
_pps: u32,
points: &[LaserPoint],
) -> DacResult<crate::backend::WriteOutcome> {
self.writes.lock().unwrap().push(points.to_vec());
self.write_count.fetch_add(1, Ordering::SeqCst);
self.points_written
.fetch_add(points.len(), Ordering::SeqCst);
Ok(crate::backend::WriteOutcome::Written)
}
}
struct FrameSwapTestBackend {
connected: bool,
write_count: Arc<AtomicUsize>,
last_frame_size: Arc<AtomicUsize>,
shutter_open: Arc<AtomicBool>,
ready: Arc<AtomicBool>,
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
frame_capacity: usize,
}
impl FrameSwapTestBackend {
fn new() -> Self {
Self {
connected: false,
write_count: Arc::new(AtomicUsize::new(0)),
last_frame_size: Arc::new(AtomicUsize::new(0)),
shutter_open: Arc::new(AtomicBool::new(false)),
ready: Arc::new(AtomicBool::new(true)),
writes: Arc::new(Mutex::new(Vec::new())),
frame_capacity: 4095,
}
}
fn new_with_capacity(frame_capacity: usize) -> Self {
Self {
frame_capacity,
..Self::new()
}
}
}
impl DacBackend for FrameSwapTestBackend {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("FrameSwapTest".into())
}
fn caps(&self) -> &crate::device::DacCapabilities {
static CAPS: crate::device::DacCapabilities = crate::device::DacCapabilities {
pps_min: 1000,
pps_max: 100000,
max_points_per_chunk: 4095,
output_model: crate::device::OutputModel::UsbFrameSwap,
};
&CAPS
}
fn connect(&mut self) -> DacResult<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> DacResult<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> DacResult<()> {
Ok(())
}
fn set_shutter(&mut self, open: bool) -> DacResult<()> {
self.shutter_open.store(open, Ordering::SeqCst);
Ok(())
}
}
impl FrameSwapBackend for FrameSwapTestBackend {
fn frame_capacity(&self) -> usize {
self.frame_capacity
}
fn is_ready_for_frame(&mut self) -> bool {
self.ready.load(Ordering::SeqCst)
}
fn write_frame(
&mut self,
_pps: u32,
points: &[LaserPoint],
) -> DacResult<crate::backend::WriteOutcome> {
self.writes.lock().unwrap().push(points.to_vec());
self.write_count.fetch_add(1, Ordering::SeqCst);
self.last_frame_size.store(points.len(), Ordering::SeqCst);
Ok(crate::backend::WriteOutcome::Written)
}
}
struct RetryFifoTestBackend {
connected: bool,
caps: crate::device::DacCapabilities,
shutter_open: Arc<AtomicBool>,
block_next_writes: Arc<AtomicUsize>,
block_next_visible_writes: Arc<AtomicUsize>,
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
}
impl RetryFifoTestBackend {
fn new() -> Self {
Self {
connected: false,
caps: crate::device::DacCapabilities {
pps_min: 1000,
pps_max: 100000,
max_points_per_chunk: 20,
output_model: crate::device::OutputModel::NetworkFifo,
},
shutter_open: Arc::new(AtomicBool::new(false)),
block_next_writes: Arc::new(AtomicUsize::new(0)),
block_next_visible_writes: Arc::new(AtomicUsize::new(0)),
writes: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl DacBackend for RetryFifoTestBackend {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("RetryFifoTest".into())
}
fn caps(&self) -> &crate::device::DacCapabilities {
&self.caps
}
fn connect(&mut self) -> DacResult<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> DacResult<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> DacResult<()> {
Ok(())
}
fn set_shutter(&mut self, open: bool) -> DacResult<()> {
self.shutter_open.store(open, Ordering::SeqCst);
Ok(())
}
}
impl FifoBackend for RetryFifoTestBackend {
fn try_write_points(
&mut self,
_pps: u32,
points: &[LaserPoint],
) -> DacResult<crate::backend::WriteOutcome> {
self.writes.lock().unwrap().push(points.to_vec());
let visible = points.iter().any(|point| point.intensity != 0);
let remaining_visible = self.block_next_visible_writes.load(Ordering::SeqCst);
if visible && remaining_visible > 0 {
self.block_next_visible_writes
.store(remaining_visible - 1, Ordering::SeqCst);
return Ok(crate::backend::WriteOutcome::WouldBlock);
}
let remaining = self.block_next_writes.load(Ordering::SeqCst);
if remaining > 0 {
self.block_next_writes
.store(remaining - 1, Ordering::SeqCst);
return Ok(crate::backend::WriteOutcome::WouldBlock);
}
Ok(crate::backend::WriteOutcome::Written)
}
fn queued_points(&self) -> Option<u64> {
Some(0)
}
}
struct DisconnectAfterNFrameSwapBackend {
connected: bool,
fail_after: usize,
write_count: AtomicUsize,
}
impl DisconnectAfterNFrameSwapBackend {
fn new(fail_after: usize) -> Self {
Self {
connected: false,
fail_after,
write_count: AtomicUsize::new(0),
}
}
}
impl DacBackend for DisconnectAfterNFrameSwapBackend {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("FilterReconnectFrameSwap".into())
}
fn caps(&self) -> &crate::device::DacCapabilities {
static CAPS: crate::device::DacCapabilities = crate::device::DacCapabilities {
pps_min: 1000,
pps_max: 100000,
max_points_per_chunk: 4095,
output_model: crate::device::OutputModel::UsbFrameSwap,
};
&CAPS
}
fn connect(&mut self) -> DacResult<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> DacResult<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> DacResult<()> {
Ok(())
}
fn set_shutter(&mut self, _: bool) -> DacResult<()> {
Ok(())
}
}
impl FrameSwapBackend for DisconnectAfterNFrameSwapBackend {
fn frame_capacity(&self) -> usize {
4095
}
fn is_ready_for_frame(&mut self) -> bool {
true
}
fn write_frame(
&mut self,
_pps: u32,
_points: &[LaserPoint],
) -> DacResult<crate::backend::WriteOutcome> {
let count = self.write_count.fetch_add(1, Ordering::SeqCst);
if count >= self.fail_after {
self.connected = false;
Err(crate::error::Error::disconnected("simulated disconnect"))
} else {
Ok(crate::backend::WriteOutcome::Written)
}
}
}
struct ReconnectFrameSwapBackend {
connected: bool,
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
}
impl ReconnectFrameSwapBackend {
fn new(writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>) -> Self {
Self {
connected: false,
writes,
}
}
}
impl DacBackend for ReconnectFrameSwapBackend {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("FilterReconnectFrameSwap".into())
}
fn caps(&self) -> &crate::device::DacCapabilities {
static CAPS: crate::device::DacCapabilities = crate::device::DacCapabilities {
pps_min: 1000,
pps_max: 100000,
max_points_per_chunk: 4095,
output_model: crate::device::OutputModel::UsbFrameSwap,
};
&CAPS
}
fn connect(&mut self) -> DacResult<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> DacResult<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> DacResult<()> {
Ok(())
}
fn set_shutter(&mut self, _: bool) -> DacResult<()> {
Ok(())
}
}
impl FrameSwapBackend for ReconnectFrameSwapBackend {
fn frame_capacity(&self) -> usize {
4095
}
fn is_ready_for_frame(&mut self) -> bool {
true
}
fn write_frame(
&mut self,
_pps: u32,
points: &[LaserPoint],
) -> DacResult<crate::backend::WriteOutcome> {
self.writes.lock().unwrap().push(points.to_vec());
Ok(crate::backend::WriteOutcome::Written)
}
}
struct FilterReconnectFrameSwapDiscoverer {
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
}
impl crate::discovery::Discoverer for FilterReconnectFrameSwapDiscoverer {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("FilterReconnectFrameSwap".into())
}
fn prefix(&self) -> &str {
"filterreconnectframeswap"
}
fn scan(&mut self) -> Vec<crate::discovery::DiscoveredDevice> {
let info = crate::discovery::DiscoveredDeviceInfo::new(
crate::device::DacType::Custom("FilterReconnectFrameSwap".into()),
"filterreconnectframeswap:10.0.0.77",
"Filter Reconnect FrameSwap",
)
.with_ip("10.0.0.77".parse().unwrap())
.with_hardware_name("Filter Reconnect FrameSwap");
vec![crate::discovery::DiscoveredDevice::new(info, Box::new(()))]
}
fn connect(&mut self, _opaque: Box<dyn std::any::Any + Send>) -> DacResult<BackendKind> {
Ok(BackendKind::FrameSwap(Box::new(
ReconnectFrameSwapBackend::new(self.writes.clone()),
)))
}
}
struct DelayedReconnectFrameSwapDiscoverer {
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
empty_scans_remaining: Arc<AtomicUsize>,
}
impl crate::discovery::Discoverer for DelayedReconnectFrameSwapDiscoverer {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("DelayedReconnectFrameSwap".into())
}
fn prefix(&self) -> &str {
"delayedreconnectframeswap"
}
fn scan(&mut self) -> Vec<crate::discovery::DiscoveredDevice> {
if self.empty_scans_remaining.load(Ordering::SeqCst) > 0 {
self.empty_scans_remaining.fetch_sub(1, Ordering::SeqCst);
return vec![];
}
let info = crate::discovery::DiscoveredDeviceInfo::new(
crate::device::DacType::Custom("DelayedReconnectFrameSwap".into()),
"delayedreconnectframeswap:10.0.0.88",
"Delayed Reconnect FrameSwap",
)
.with_ip("10.0.0.88".parse().unwrap())
.with_hardware_name("Delayed Reconnect FrameSwap");
vec![crate::discovery::DiscoveredDevice::new(info, Box::new(()))]
}
fn connect(&mut self, _opaque: Box<dyn std::any::Any + Send>) -> DacResult<BackendKind> {
Ok(BackendKind::FrameSwap(Box::new(
ReconnectFrameSwapBackend::new(self.writes.clone()),
)))
}
}
struct RetryFrameSwapTestBackend {
connected: bool,
caps: crate::device::DacCapabilities,
shutter_open: Arc<AtomicBool>,
ready: Arc<AtomicBool>,
block_next_writes: Arc<AtomicUsize>,
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
}
impl RetryFrameSwapTestBackend {
fn new() -> Self {
Self {
connected: false,
caps: crate::device::DacCapabilities {
pps_min: 1000,
pps_max: 100000,
max_points_per_chunk: 4095,
output_model: crate::device::OutputModel::UsbFrameSwap,
},
shutter_open: Arc::new(AtomicBool::new(false)),
ready: Arc::new(AtomicBool::new(true)),
block_next_writes: Arc::new(AtomicUsize::new(0)),
writes: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl DacBackend for RetryFrameSwapTestBackend {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("RetryFrameSwapTest".into())
}
fn caps(&self) -> &crate::device::DacCapabilities {
&self.caps
}
fn connect(&mut self) -> DacResult<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> DacResult<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> DacResult<()> {
Ok(())
}
fn set_shutter(&mut self, open: bool) -> DacResult<()> {
self.shutter_open.store(open, Ordering::SeqCst);
Ok(())
}
}
impl FrameSwapBackend for RetryFrameSwapTestBackend {
fn frame_capacity(&self) -> usize {
self.caps.max_points_per_chunk
}
fn is_ready_for_frame(&mut self) -> bool {
self.ready.load(Ordering::SeqCst)
}
fn write_frame(
&mut self,
_pps: u32,
points: &[LaserPoint],
) -> DacResult<crate::backend::WriteOutcome> {
self.writes.lock().unwrap().push(points.to_vec());
let remaining = self.block_next_writes.load(Ordering::SeqCst);
if remaining > 0 {
self.block_next_writes
.store(remaining - 1, Ordering::SeqCst);
return Ok(crate::backend::WriteOutcome::WouldBlock);
}
Ok(crate::backend::WriteOutcome::Written)
}
}
struct RetryUdpTimedTestBackend {
connected: bool,
caps: crate::device::DacCapabilities,
shutter_open: Arc<AtomicBool>,
block_next_writes: Arc<AtomicUsize>,
writes: Arc<Mutex<Vec<Vec<LaserPoint>>>>,
}
impl RetryUdpTimedTestBackend {
fn new() -> Self {
Self {
connected: false,
caps: crate::device::DacCapabilities {
pps_min: 1000,
pps_max: 100000,
max_points_per_chunk: 3,
output_model: crate::device::OutputModel::UdpTimed,
},
shutter_open: Arc::new(AtomicBool::new(false)),
block_next_writes: Arc::new(AtomicUsize::new(0)),
writes: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl DacBackend for RetryUdpTimedTestBackend {
fn dac_type(&self) -> crate::device::DacType {
crate::device::DacType::Custom("RetryUdpTimedTest".into())
}
fn caps(&self) -> &crate::device::DacCapabilities {
&self.caps
}
fn connect(&mut self) -> DacResult<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> DacResult<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> DacResult<()> {
Ok(())
}
fn set_shutter(&mut self, open: bool) -> DacResult<()> {
self.shutter_open.store(open, Ordering::SeqCst);
Ok(())
}
}
impl FifoBackend for RetryUdpTimedTestBackend {
fn try_write_points(
&mut self,
_pps: u32,
points: &[LaserPoint],
) -> DacResult<crate::backend::WriteOutcome> {
self.writes.lock().unwrap().push(points.to_vec());
let remaining = self.block_next_writes.load(Ordering::SeqCst);
if remaining > 0 {
self.block_next_writes
.store(remaining - 1, Ordering::SeqCst);
return Ok(crate::backend::WriteOutcome::WouldBlock);
}
Ok(crate::backend::WriteOutcome::Written)
}
}
fn wait_for_writes(
writes: &Arc<Mutex<Vec<Vec<LaserPoint>>>>,
min_len: usize,
timeout: std::time::Duration,
) {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if writes.lock().unwrap().len() >= min_len {
return;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!(
"timed out waiting for {} writes, got {}",
min_len,
writes.lock().unwrap().len()
);
}
fn wait_for_loop_activity_after(
metrics: &FrameSessionMetrics,
after: std::time::Instant,
timeout: std::time::Duration,
) -> std::time::Instant {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if let Some(activity) = metrics.last_loop_activity() {
if activity > after {
return activity;
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!("timed out waiting for loop activity after {after:?}");
}
fn wait_for_write_success(
metrics: &FrameSessionMetrics,
timeout: std::time::Duration,
) -> std::time::Instant {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if let Some(write_success) = metrics.last_write_success() {
return write_success;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!("timed out waiting for write success");
}
fn wait_for_write_success_after(
metrics: &FrameSessionMetrics,
after: std::time::Instant,
timeout: std::time::Duration,
) -> std::time::Instant {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if let Some(write_success) = metrics.last_write_success() {
if write_success > after {
return write_success;
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!("timed out waiting for write success after {after:?}");
}
fn encode_transition() -> TransitionFn {
Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
})
}
fn is_encoded_frame_swap_transition(points: &[LaserPoint], from_x: f32, to_x: f32) -> bool {
points.len() == 2
&& points[0].x == from_x
&& points[0].y == to_x
&& points[0].intensity == 0
&& points[1].x == to_x
&& points[1].intensity == 65535
}
fn is_transition_bearing_udp_chunk(points: &[LaserPoint]) -> bool {
points.iter().any(|p| p.intensity == 0) && points.iter().any(|p| p.intensity != 0)
}
#[test]
fn test_frame_session_fifo_submit_frame_writes_points() {
let backend = FifoTestBackend::new();
let points_written = backend.points_written.clone();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let config = FrameSessionConfig::new(30000).with_startup_blank(std::time::Duration::ZERO);
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![
LaserPoint::new(0.0, 0.0, 65535, 0, 0, 65535),
LaserPoint::new(1.0, 0.0, 0, 65535, 0, 65535),
]));
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(
points_written.load(Ordering::SeqCst) > 0,
"Should have written points"
);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for FIFO frame session, got {exit:?}"
);
}
#[test]
fn test_frame_session_frame_swap_writes_frames() {
let backend = FrameSwapTestBackend::new();
let write_count = backend.write_count.clone();
let last_frame_size = backend.last_frame_size.clone();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let config = FrameSessionConfig::new(30000).with_startup_blank(std::time::Duration::ZERO);
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![
LaserPoint::new(0.0, 0.0, 65535, 0, 0, 65535),
LaserPoint::new(1.0, 0.0, 0, 65535, 0, 65535),
]));
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(
write_count.load(Ordering::SeqCst) > 0,
"Should have written frames"
);
assert!(
last_frame_size.load(Ordering::SeqCst) > 0,
"Frame should have points"
);
session.control().stop().unwrap();
let exit = session.join().unwrap();
assert_eq!(exit, RunExit::Stopped);
}
#[test]
fn test_frame_session_frame_swap_retries_same_inflight_frame_on_wouldblock() {
let backend = RetryFrameSwapTestBackend::new();
let writes = backend.writes.clone();
let ready = backend.ready.clone();
let block_next_writes = backend.block_next_writes.clone();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let config = FrameSessionConfig::new(30_000)
.with_transition_fn(encode_transition())
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0);
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
wait_for_writes(&writes, 1, std::time::Duration::from_millis(200));
ready.store(false, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(10));
let before = writes.lock().unwrap().len();
block_next_writes.store(1, Ordering::SeqCst);
session.send_frame(Frame::new(vec![make_point(5.0, 0.0)]));
ready.store(true, Ordering::SeqCst);
wait_for_writes(&writes, before + 2, std::time::Duration::from_millis(200));
let writes = writes.lock().unwrap();
let first = &writes[before];
let second = &writes[before + 1];
assert_eq!(
first, second,
"inflight A→B frame should be retried verbatim"
);
assert!(
is_encoded_frame_swap_transition(first, 1.0, 5.0),
"expected encoded A→B frame, got {:?}",
first
);
drop(writes);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for UDP-timed session, got {exit:?}"
);
}
#[test]
fn test_frame_session_frame_swap_inflight_frame_stays_sticky_until_accepted() {
let backend = RetryFrameSwapTestBackend::new();
let writes = backend.writes.clone();
let ready = backend.ready.clone();
let block_next_writes = backend.block_next_writes.clone();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let config = FrameSessionConfig::new(30_000)
.with_transition_fn(encode_transition())
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0);
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
wait_for_writes(&writes, 1, std::time::Duration::from_millis(200));
ready.store(false, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(10));
let before = writes.lock().unwrap().len();
block_next_writes.store(3, Ordering::SeqCst);
session.send_frame(Frame::new(vec![make_point(5.0, 0.0)]));
ready.store(true, Ordering::SeqCst);
wait_for_writes(&writes, before + 1, std::time::Duration::from_millis(200));
session.send_frame(Frame::new(vec![make_point(9.0, 0.0)]));
wait_for_writes(&writes, before + 5, std::time::Duration::from_millis(300));
let writes = writes.lock().unwrap();
for attempt in &writes[before..before + 4] {
assert!(
is_encoded_frame_swap_transition(attempt, 1.0, 5.0),
"expected sticky A→B frame, got {:?}",
attempt
);
}
let next = &writes[before + 4];
assert!(
is_encoded_frame_swap_transition(next, 5.0, 9.0),
"expected B→C after A→B was accepted, got {:?}",
next
);
drop(writes);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for UDP-timed session, got {exit:?}"
);
}
#[test]
fn test_frame_session_udp_timed_retries_same_transition_chunk_on_wouldblock() {
let backend = RetryUdpTimedTestBackend::new();
let writes = backend.writes.clone();
let block_next_writes = backend.block_next_writes.clone();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let config = FrameSessionConfig::new(1_000)
.with_transition_fn(encode_transition())
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0);
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
wait_for_writes(&writes, 1, std::time::Duration::from_millis(200));
let before = writes.lock().unwrap().len();
block_next_writes.store(1, Ordering::SeqCst);
session.send_frame(Frame::new(vec![make_point(5.0, 0.0)]));
wait_for_writes(&writes, before + 8, std::time::Duration::from_millis(400));
let writes = writes.lock().unwrap();
let retry_idx = writes[before..]
.windows(2)
.position(|window| window[0] == window[1] && is_transition_bearing_udp_chunk(&window[0]))
.unwrap_or_else(|| {
panic!(
"expected a retried transition-bearing UDP chunk, got {:?}",
&writes[before..]
)
});
let first = &writes[before + retry_idx];
assert!(
is_transition_bearing_udp_chunk(first),
"expected a transition-bearing UDP chunk, got {:?}",
first
);
if before + retry_idx + 2 < writes.len() {
assert_ne!(
first,
&writes[before + retry_idx + 2],
"retry should preserve one specific chunk, not collapse the whole output into a constant pattern"
);
}
drop(writes);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for UDP-timed session, got {exit:?}"
);
}
#[test]
fn test_frame_session_fifo_output_filter_skips_keepalives_and_sees_color_delay() {
let backend = RetryUdpTimedTestBackend::new();
let writes = backend.writes.clone();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let (filter, observations) = RecordingFilter::new();
let config = FrameSessionConfig::new(1_000)
.with_transition_fn(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}))
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(1)
.with_output_filter(Box::new(filter));
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
wait_for_writes(&writes, 1, std::time::Duration::from_millis(200));
assert_eq!(
observations.calls.load(Ordering::SeqCst),
0,
"pre-first-frame FIFO keepalives must not invoke the output filter"
);
session.send_frame(Frame::new(vec![
LaserPoint::new(0.0, 0.0, 100, 200, 300, 400),
LaserPoint::new(0.5, 0.0, 500, 600, 700, 800),
LaserPoint::new(1.0, 0.0, 900, 1000, 1100, 1200),
]));
wait_for_filter_calls(&observations, 1, std::time::Duration::from_millis(200));
let call = observations.invocations.lock().unwrap()[0].clone();
assert_eq!(call.ctx.kind, PresentedSliceKind::FifoChunk);
assert!(!call.ctx.is_cyclic);
assert_eq!(call.points.len(), 3);
assert_eq!(call.points[0].r, 0);
assert_eq!(call.points[0].g, 0);
assert_eq!(call.points[0].b, 0);
assert_eq!(call.points[0].intensity, 0);
assert_eq!(call.points[1].r, 100);
assert_eq!(call.points[1].g, 200);
assert_eq!(call.points[1].b, 300);
assert_eq!(call.points[1].intensity, 400);
assert_eq!(call.points[2].r, 500);
assert_eq!(call.points[2].g, 600);
assert_eq!(call.points[2].b, 700);
assert_eq!(call.points[2].intensity, 800);
assert!(
writes.lock().unwrap().contains(&call.points),
"backend should receive the same filtered FIFO chunk"
);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for UDP-timed session, got {exit:?}"
);
}
#[test]
fn test_frame_session_frame_swap_output_filter_sees_post_clamp_cyclic_frame() {
let backend = FrameSwapTestBackend::new_with_capacity(3);
let writes = backend.writes.clone();
let write_count = backend.write_count.clone();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let (filter, observations) = RecordingFilter::with_intensity_tag(1234);
let config = FrameSessionConfig::new(30_000)
.with_transition_fn(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![
LaserPoint::blanked(10.0, 0.0),
LaserPoint::blanked(20.0, 0.0),
])
}))
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0)
.with_output_filter(Box::new(filter));
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
wait_for_filter_calls(&observations, 1, std::time::Duration::from_millis(200));
let before_writes = write_count.load(Ordering::SeqCst);
session.send_frame(Frame::new(vec![
make_point(30.0, 0.0),
make_point(40.0, 0.0),
]));
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(200);
let call = loop {
if let Some(call) = observations
.invocations
.lock()
.unwrap()
.iter()
.find(|call| call.points.len() == 3 && call.points[1].x == 30.0)
.cloned()
{
break call;
}
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for the clamped A->B frame"
);
std::thread::sleep(std::time::Duration::from_millis(1));
};
while write_count.load(Ordering::SeqCst) <= before_writes {
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert_eq!(call.ctx.kind, PresentedSliceKind::FrameSwapFrame);
assert!(call.ctx.is_cyclic);
assert_eq!(call.points.len(), 3);
assert_eq!(
call.points[0].x, 20.0,
"clamp should trim the first transition point"
);
assert_eq!(call.points[1].x, 30.0);
assert_eq!(call.points[2].x, 40.0);
assert!(call.points.iter().all(|point| point.intensity == 1234));
assert!(
writes.lock().unwrap().contains(&call.points),
"backend should receive the filtered frame-swap buffer verbatim"
);
session.control().stop().unwrap();
session.join().unwrap();
}
#[test]
fn test_frame_session_output_filter_resets_on_arm_and_disarm_and_sees_blanking() {
let backend = FrameSwapTestBackend::new();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let (filter, observations) = RecordingFilter::new();
let config = FrameSessionConfig::new(1_000)
.with_transition_fn(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}))
.with_startup_blank(std::time::Duration::from_millis(2))
.with_color_delay_points(0)
.with_output_filter(Box::new(filter));
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.send_frame(Frame::new(vec![
make_point(1.0, 0.0),
make_point(2.0, 0.0),
make_point(3.0, 0.0),
]));
wait_for_filter_calls(&observations, 1, std::time::Duration::from_millis(200));
assert_eq!(
observations.resets.lock().unwrap().as_slice(),
&[OutputResetReason::SessionStart]
);
let before_arm = observations.calls.load(Ordering::SeqCst);
session.control().arm().unwrap();
wait_for_filter_reset(
&observations,
OutputResetReason::Arm,
std::time::Duration::from_millis(200),
);
let armed_deadline = std::time::Instant::now() + std::time::Duration::from_millis(200);
let armed = loop {
if let Some(call) = observations
.invocations
.lock()
.unwrap()
.iter()
.skip(before_arm)
.find(|call| {
call.points.len() >= 3
&& call.points[0].intensity == 0
&& call.points[1].intensity == 0
&& call.points[2].intensity != 0
})
.cloned()
{
break call;
}
assert!(
std::time::Instant::now() < armed_deadline,
"timed out waiting for post-arm startup blanking output"
);
std::thread::sleep(std::time::Duration::from_millis(1));
};
assert_eq!(armed.points[0].intensity, 0);
assert_eq!(armed.points[1].intensity, 0);
assert_eq!(armed.points[2].intensity, 65535);
let before_disarm = observations.calls.load(Ordering::SeqCst);
session.control().disarm().unwrap();
wait_for_filter_reset(
&observations,
OutputResetReason::Disarm,
std::time::Duration::from_millis(200),
);
let disarmed_deadline = std::time::Instant::now() + std::time::Duration::from_millis(200);
let disarmed = loop {
if let Some(call) = observations
.invocations
.lock()
.unwrap()
.iter()
.skip(before_disarm)
.find(|call| call.points.iter().all(|point| point.intensity == 0))
.cloned()
{
break call;
}
assert!(
std::time::Instant::now() < disarmed_deadline,
"timed out waiting for disarmed blank output"
);
std::thread::sleep(std::time::Duration::from_millis(1));
};
assert!(disarmed.points.iter().all(|point| point.intensity == 0));
let before_rearm = observations.calls.load(Ordering::SeqCst);
session.control().arm().unwrap();
let rearmed_deadline = std::time::Instant::now() + std::time::Duration::from_millis(200);
let rearmed = loop {
if let Some(call) = observations
.invocations
.lock()
.unwrap()
.iter()
.skip(before_rearm)
.find(|call| {
call.points.len() >= 3
&& call.points[0].intensity == 0
&& call.points[1].intensity == 0
&& call.points[2].intensity != 0
})
.cloned()
{
break call;
}
assert!(
std::time::Instant::now() < rearmed_deadline,
"timed out waiting for post-rearm startup blanking output"
);
std::thread::sleep(std::time::Duration::from_millis(1));
};
assert_eq!(rearmed.points[0].intensity, 0);
assert_eq!(rearmed.points[1].intensity, 0);
assert_eq!(rearmed.points[2].intensity, 65535);
assert_eq!(
observations.resets.lock().unwrap().as_slice(),
&[
OutputResetReason::SessionStart,
OutputResetReason::Arm,
OutputResetReason::Disarm,
OutputResetReason::Arm,
]
);
session.control().stop().unwrap();
session.join().unwrap();
}
#[test]
fn test_frame_session_frame_swap_output_filter_not_rerun_for_retry() {
let backend = RetryFrameSwapTestBackend::new();
let writes = backend.writes.clone();
let ready = backend.ready.clone();
let block_next_writes = backend.block_next_writes.clone();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let (filter, observations) = RecordingFilter::new();
let config = FrameSessionConfig::new(30_000)
.with_transition_fn(encode_transition())
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0)
.with_output_filter(Box::new(filter));
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
wait_for_writes(&writes, 1, std::time::Duration::from_millis(200));
wait_for_filter_calls(&observations, 1, std::time::Duration::from_millis(200));
ready.store(false, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(10));
let before_writes = writes.lock().unwrap().len();
block_next_writes.store(1, Ordering::SeqCst);
session.send_frame(Frame::new(vec![make_point(5.0, 0.0)]));
ready.store(true, Ordering::SeqCst);
wait_for_writes(
&writes,
before_writes + 2,
std::time::Duration::from_millis(200),
);
let retried_frame = {
let writes = writes.lock().unwrap();
writes[before_writes].clone()
};
assert_eq!(
observations
.invocations
.lock()
.unwrap()
.iter()
.filter(|call| call.points == retried_frame)
.count(),
1,
"retrying the same frame-swap write must not rerun the filter for that frame"
);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for frame-swap session, got {exit:?}"
);
}
#[test]
fn test_frame_session_fifo_output_filter_not_rerun_for_inner_retry() {
let backend = RetryFifoTestBackend::new();
let writes = backend.writes.clone();
let block_next_visible_writes = backend.block_next_visible_writes.clone();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let (filter, observations) = StampingFilter::new();
let config = FrameSessionConfig::new(1_000)
.with_transition_fn(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}))
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0)
.with_output_filter(Box::new(filter));
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().arm().unwrap();
block_next_visible_writes.store(1, Ordering::SeqCst);
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
wait_for_filter_calls(&observations, 1, std::time::Duration::from_millis(200));
let retried_chunk = observations.invocations.lock().unwrap()[0].points.clone();
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(200);
while std::time::Instant::now() < deadline {
let duplicated = writes
.lock()
.unwrap()
.iter()
.filter(|write| **write == retried_chunk)
.count();
if duplicated >= 2 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(
writes
.lock()
.unwrap()
.iter()
.filter(|write| **write == retried_chunk)
.count()
>= 2,
"expected the same stamped FIFO chunk to be written twice after WouldBlock"
);
let stamp = retried_chunk[0].intensity;
assert_eq!(
observations
.invocations
.lock()
.unwrap()
.iter()
.filter(|call| call.points[0].intensity == stamp)
.count(),
1,
"inner WouldBlock retry must reuse the already-filtered FIFO chunk"
);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for FIFO session, got {exit:?}"
);
}
#[test]
fn test_frame_session_arm_disarm() {
let backend = FifoTestBackend::new();
let shutter = backend.shutter_open.clone();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let config = FrameSessionConfig::new(30000);
let session = FrameSession::start(backend_kind, config, None).unwrap();
assert!(!session.control().is_armed());
session.control().arm().unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
assert!(shutter.load(Ordering::SeqCst));
session.control().disarm().unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
assert!(!shutter.load(Ordering::SeqCst));
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for arm/disarm session, got {exit:?}"
);
}
#[test]
fn test_frame_session_stop() {
let backend = FifoTestBackend::new();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let config = FrameSessionConfig::new(30000);
let session = FrameSession::start(backend_kind, config, None).unwrap();
session.control().stop().unwrap();
let exit = session.join().unwrap();
assert_eq!(exit, RunExit::Stopped);
}
#[test]
fn test_frame_session_metrics_available_and_disconnect_after_stop() {
let backend = FifoTestBackend::new();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let config = FrameSessionConfig::new(30_000);
let session = FrameSession::start(backend_kind, config, None).unwrap();
let metrics = session.metrics();
assert!(metrics.connected());
assert!(metrics.last_loop_activity().is_some());
let write_success = wait_for_write_success(&metrics, std::time::Duration::from_millis(200));
assert!(write_success <= metrics.last_write_success().unwrap());
session.control().stop().unwrap();
session.join().unwrap();
assert!(!metrics.connected());
}
#[test]
fn test_frame_session_fifo_metrics_advance_while_sleeping_with_healthy_buffer() {
let backend = FifoTestBackend::new();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let config = FrameSessionConfig::new(30).with_startup_blank(std::time::Duration::ZERO);
let session = FrameSession::start(backend_kind, config, None).unwrap();
let metrics = session.metrics();
let first_write = wait_for_write_success(&metrics, std::time::Duration::from_millis(200));
let later_activity =
wait_for_loop_activity_after(&metrics, first_write, std::time::Duration::from_millis(200));
assert!(later_activity > first_write);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for FIFO session, got {exit:?}"
);
}
#[test]
fn test_frame_session_udp_timed_metrics_advance_while_retrying_same_chunk() {
let backend = RetryUdpTimedTestBackend::new();
let writes = backend.writes.clone();
let block_next_writes = backend.block_next_writes.clone();
let backend_kind = BackendKind::Fifo(Box::new(backend));
let config = FrameSessionConfig::new(1_000)
.with_transition_fn(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}))
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0);
let session = FrameSession::start(backend_kind, config, None).unwrap();
let metrics = session.metrics();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
let baseline_write = wait_for_write_success(&metrics, std::time::Duration::from_millis(200));
let before_writes = writes.lock().unwrap().len();
let before_activity = metrics.last_loop_activity().unwrap();
block_next_writes.store(20, Ordering::SeqCst);
wait_for_writes(
&writes,
before_writes + 2,
std::time::Duration::from_millis(200),
);
assert!(
metrics.last_loop_activity().unwrap() > before_activity,
"loop activity should advance while retrying a WouldBlock UDP chunk"
);
assert_eq!(
metrics.last_write_success(),
Some(baseline_write),
"write-success timestamp must not advance on WouldBlock retries"
);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for UDP-timed session, got {exit:?}"
);
}
#[test]
fn test_frame_session_frame_swap_metrics_advance_while_waiting_for_readiness() {
let backend = RetryFrameSwapTestBackend::new();
let ready = backend.ready.clone();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let config = FrameSessionConfig::new(30_000).with_startup_blank(std::time::Duration::ZERO);
ready.store(false, Ordering::SeqCst);
let session = FrameSession::start(backend_kind, config, None).unwrap();
let metrics = session.metrics();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
let before = metrics.last_loop_activity().unwrap();
let after =
wait_for_loop_activity_after(&metrics, before, std::time::Duration::from_millis(200));
assert!(after > before);
assert_eq!(metrics.last_write_success(), None);
session.control().stop().unwrap();
session.join().unwrap();
}
#[test]
fn test_frame_session_metrics_write_success_only_advances_on_successful_write() {
let backend = RetryFrameSwapTestBackend::new();
let writes = backend.writes.clone();
let ready = backend.ready.clone();
let block_next_writes = backend.block_next_writes.clone();
let backend_kind = BackendKind::FrameSwap(Box::new(backend));
let config = FrameSessionConfig::new(30_000)
.with_transition_fn(encode_transition())
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0);
let session = FrameSession::start(backend_kind, config, None).unwrap();
let metrics = session.metrics();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
let first_write = wait_for_write_success(&metrics, std::time::Duration::from_millis(200));
assert!(first_write <= metrics.last_write_success().unwrap());
ready.store(false, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(10));
let baseline_write = metrics.last_write_success().unwrap();
let before_writes = writes.lock().unwrap().len();
block_next_writes.store(3, Ordering::SeqCst);
session.send_frame(Frame::new(vec![make_point(5.0, 0.0)]));
ready.store(true, Ordering::SeqCst);
wait_for_writes(
&writes,
before_writes + 2,
std::time::Duration::from_millis(200),
);
assert_eq!(
metrics.last_write_success(),
Some(baseline_write),
"WouldBlock retries must not look like successful writes"
);
let next_write = wait_for_write_success_after(
&metrics,
baseline_write,
std::time::Duration::from_millis(200),
);
assert!(next_write > baseline_write);
session.control().stop().unwrap();
let exit = session.join();
assert!(
matches!(
exit,
Ok(RunExit::Stopped) | Err(crate::error::Error::Stopped)
),
"expected clean stop for frame-swap session, got {exit:?}"
);
}
#[test]
fn test_color_delay_line_carries_across_chunks() {
use super::ColorDelayLine;
let mut delay_line = ColorDelayLine::new(1);
let mut chunk1 = vec![
LaserPoint::new(0.0, 0.0, 100, 200, 300, 400),
LaserPoint::new(1.0, 0.0, 500, 600, 700, 800),
LaserPoint::new(2.0, 0.0, 900, 1000, 1100, 1200),
];
delay_line.apply(&mut chunk1);
assert_eq!(chunk1[0].r, 0);
assert_eq!(chunk1[1].r, 100);
assert_eq!(chunk1[2].r, 500);
let mut chunk2 = vec![
LaserPoint::new(3.0, 0.0, 1300, 1400, 1500, 1600),
LaserPoint::new(4.0, 0.0, 1700, 1800, 1900, 2000),
];
delay_line.apply(&mut chunk2);
assert_eq!(chunk2[0].r, 900);
assert_eq!(chunk2[0].g, 1000);
assert_eq!(chunk2[0].b, 1100);
assert_eq!(chunk2[0].intensity, 1200);
assert_eq!(chunk2[1].r, 1300);
assert_eq!(chunk2[0].x, 3.0);
assert_eq!(chunk2[1].x, 4.0);
}
#[test]
fn test_color_delay_line_zero_delay_is_noop() {
use super::ColorDelayLine;
let mut delay_line = ColorDelayLine::new(0);
let mut points = vec![LaserPoint::new(0.0, 0.0, 100, 200, 300, 400)];
delay_line.apply(&mut points);
assert_eq!(points[0].r, 100);
assert_eq!(points[0].g, 200);
}
#[test]
fn test_engine_empty_transition_result() {
let mut engine = make_engine_no_transition();
let frame = make_frame(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]);
engine.set_pending(frame);
let mut buffer = vec![LaserPoint::default(); 4];
engine.fill_chunk(&mut buffer, 4);
assert_eq!(buffer[0].x, 1.0);
assert_eq!(buffer[1].x, 2.0);
assert_eq!(buffer[2].x, 1.0);
assert_eq!(buffer[3].x, 2.0);
}
#[test]
fn test_compose_hardware_frame_natural_length_no_padding() {
let mut engine = make_engine(); let frame = make_frame(vec![
make_point(0.0, 0.0),
make_point(0.2, 0.0),
make_point(0.4, 0.0),
make_point(0.6, 0.0),
make_point(0.8, 0.0),
]);
engine.set_pending(frame);
let composed = engine.compose_hardware_frame();
assert_eq!(
composed.len(),
7,
"frame should be at natural length, not padded"
);
}
#[test]
fn test_compose_hardware_frame_content_not_cycled() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![LaserPoint::blanked(from.x, to.x)])
}));
let frame = make_frame(vec![
make_point(1.0, 0.0),
make_point(2.0, 0.0),
make_point(3.0, 0.0),
]);
engine.set_pending(frame);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 4);
assert_eq!(composed[1].x, 1.0);
assert_eq!(composed[2].x, 2.0);
assert_eq!(composed[3].x, 3.0);
}
#[test]
fn test_compose_hardware_frame_short_frame_preserves_duty_cycle() {
let mut engine = make_engine(); let frame = make_frame(vec![make_point(0.5, 0.0), make_point(-0.5, 0.0)]);
engine.set_pending(frame);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 4, "no extra padding points");
assert_eq!(composed[0].intensity, 0);
assert_eq!(composed[1].intensity, 0);
assert_eq!(composed[2].intensity, 65535);
assert_eq!(composed[3].intensity, 65535);
}
#[test]
fn test_compose_hardware_frame_a_to_b_no_cycling_artifact() {
let mut engine = PresentationEngine::new(Box::new(|from: &LaserPoint, to: &LaserPoint| {
TransitionPlan::Transition(vec![
LaserPoint::blanked(from.x, from.y),
LaserPoint::blanked(to.x, to.y),
])
}));
let frame_a = make_frame(vec![make_point(-1.0, 0.0)]);
engine.set_pending(frame_a);
let _ = engine.compose_hardware_frame();
let frame_b = make_frame(vec![make_point(1.0, 0.0), make_point(1.0, 1.0)]);
engine.set_pending(frame_b);
let composed = engine.compose_hardware_frame();
assert_eq!(composed.len(), 4, "no cycling beyond transition + frame");
assert_eq!(composed[3].x, 1.0);
assert_eq!(composed[3].y, 1.0);
}
#[test]
fn test_frame_session_output_filter_resets_on_reconnect_and_replays_last_frame() {
use crate::config::ReconnectConfig;
use crate::device::{DacInfo, DacType};
use crate::stream::Dac;
let reconnect_writes = Arc::new(Mutex::new(Vec::new()));
let reconnect_writes_factory = reconnect_writes.clone();
let initial_backend = DisconnectAfterNFrameSwapBackend::new(1);
let info = DacInfo {
id: "filterreconnectframeswap:10.0.0.77".to_string(),
name: "Filter Reconnect FrameSwap".to_string(),
kind: DacType::Custom("FilterReconnectFrameSwap".to_string()),
caps: initial_backend.caps().clone(),
};
let device = Dac::new(info, BackendKind::FrameSwap(Box::new(initial_backend)))
.with_discovery_factory(move || {
let mut discovery =
crate::discovery::DacDiscovery::new(crate::device::EnabledDacTypes::none());
discovery.register(Box::new(FilterReconnectFrameSwapDiscoverer {
writes: reconnect_writes_factory.clone(),
}));
discovery
});
let (filter, observations) = RecordingFilter::new();
let config = FrameSessionConfig::new(1_000)
.with_transition_fn(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}))
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0)
.with_output_filter(Box::new(filter))
.with_reconnect(
ReconnectConfig::new()
.max_retries(3)
.backoff(std::time::Duration::from_millis(20)),
);
let (session, _info) = device.start_frame_session(config).unwrap();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]));
wait_for_filter_reset(
&observations,
OutputResetReason::Reconnect,
std::time::Duration::from_millis(500),
);
wait_for_filter_calls(&observations, 2, std::time::Duration::from_millis(500));
let resets = observations.resets.lock().unwrap().clone();
assert_eq!(resets[0], OutputResetReason::SessionStart);
assert!(resets.contains(&OutputResetReason::Arm));
assert!(resets.contains(&OutputResetReason::Reconnect));
let invocations = observations.invocations.lock().unwrap().clone();
let first = &invocations[0].points;
assert!(
invocations[1..].iter().any(|call| call.points == *first),
"replayed last frame should flow through the filter again after reconnect"
);
assert!(
!reconnect_writes.lock().unwrap().is_empty(),
"reconnected backend should receive replayed output"
);
drop(session);
}
#[test]
fn test_frame_session_metrics_advance_during_reconnect_backoff() {
use crate::config::ReconnectConfig;
use crate::device::{DacInfo, DacType};
use crate::stream::Dac;
let reconnect_writes = Arc::new(Mutex::new(Vec::new()));
let reconnect_writes_factory = reconnect_writes.clone();
let empty_scans_remaining = Arc::new(AtomicUsize::new(3));
let empty_scans_remaining_factory = empty_scans_remaining.clone();
let initial_backend = DisconnectAfterNFrameSwapBackend::new(0);
let info = DacInfo {
id: "delayedreconnectframeswap:10.0.0.88".to_string(),
name: "Delayed Reconnect FrameSwap".to_string(),
kind: DacType::Custom("DelayedReconnectFrameSwap".to_string()),
caps: initial_backend.caps().clone(),
};
let device = Dac::new(info, BackendKind::FrameSwap(Box::new(initial_backend)))
.with_discovery_factory(move || {
let mut discovery =
crate::discovery::DacDiscovery::new(crate::device::EnabledDacTypes::none());
discovery.register(Box::new(DelayedReconnectFrameSwapDiscoverer {
writes: reconnect_writes_factory.clone(),
empty_scans_remaining: empty_scans_remaining_factory.clone(),
}));
discovery
});
let config = FrameSessionConfig::new(1_000)
.with_transition_fn(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}))
.with_startup_blank(std::time::Duration::ZERO)
.with_color_delay_points(0)
.with_reconnect(
ReconnectConfig::new()
.max_retries(10)
.backoff(std::time::Duration::from_millis(20)),
);
let (session, _info) = device.start_frame_session(config).unwrap();
let metrics = session.metrics();
session.control().arm().unwrap();
session.send_frame(Frame::new(vec![make_point(1.0, 0.0)]));
let disconnect_deadline = std::time::Instant::now() + std::time::Duration::from_millis(200);
while metrics.connected() {
assert!(
std::time::Instant::now() < disconnect_deadline,
"timed out waiting for reconnect state"
);
std::thread::sleep(std::time::Duration::from_millis(1));
}
let before = metrics.last_loop_activity().unwrap();
let progress_deadline = std::time::Instant::now() + std::time::Duration::from_millis(250);
loop {
let activity = metrics.last_loop_activity().unwrap();
if activity > before {
break;
}
assert!(
!metrics.connected(),
"reconnect should still be in progress while checking liveness"
);
assert!(
std::time::Instant::now() < progress_deadline,
"timed out waiting for reconnect liveness progress"
);
std::thread::sleep(std::time::Duration::from_millis(1));
}
drop(session);
}
#[test]
fn test_engine_reset_clears_state() {
use super::engine::PresentationEngine;
let mut engine = PresentationEngine::new(default_transition(30_000));
let frame = Frame::new(vec![make_point(1.0, 0.0), make_point(2.0, 0.0)]);
engine.set_pending(frame);
let mut buffer = vec![LaserPoint::default(); 3];
engine.fill_chunk(&mut buffer, 3);
assert!(engine.current_base.is_some());
engine.reset();
assert!(engine.current_base.is_none());
assert!(engine.pending_base.is_none());
let mut buffer2 = vec![LaserPoint::default(); 2];
let n = engine.fill_chunk(&mut buffer2, 2);
assert_eq!(n, 2);
assert_eq!(buffer2[0].r, 0);
assert_eq!(buffer2[0].g, 0);
}
#[test]
fn test_engine_reset_then_replay_frame() {
use super::engine::PresentationEngine;
let mut engine = PresentationEngine::new(Box::new(|_: &LaserPoint, _: &LaserPoint| {
TransitionPlan::Transition(vec![])
}));
let frame = Frame::new(vec![make_point(1.0, 0.0)]);
engine.set_pending(frame.clone());
let mut buffer = vec![LaserPoint::default(); 2];
engine.fill_chunk(&mut buffer, 2);
engine.reset();
engine.set_pending(frame);
let mut buffer2 = vec![LaserPoint::default(); 2];
let n = engine.fill_chunk(&mut buffer2, 2);
assert_eq!(n, 2);
assert_eq!(buffer2[0].x, 1.0);
}
#[test]
fn test_color_delay_reset_clears_carry() {
use super::engine::ColorDelayLine;
let mut delay = ColorDelayLine::new(3);
let mut points = vec![
LaserPoint::new(0.0, 0.0, 65535, 0, 0, 65535),
LaserPoint::new(0.1, 0.0, 32000, 0, 0, 32000),
LaserPoint::new(0.2, 0.0, 16000, 0, 0, 16000),
];
delay.apply(&mut points);
delay.reset();
let mut points2 = vec![
LaserPoint::new(0.0, 0.0, 65535, 0, 0, 65535),
LaserPoint::new(0.1, 0.0, 65535, 0, 0, 65535),
LaserPoint::new(0.2, 0.0, 65535, 0, 0, 65535),
];
delay.apply(&mut points2);
assert_eq!(points2[0].r, 0);
assert_eq!(points2[0].intensity, 0);
assert_eq!(points2[1].r, 0);
assert_eq!(points2[2].r, 0);
}
#[test]
fn test_frame_fifo_buffer_estimation_matches_shared_scheduler_helper() {
assert_eq!(
crate::scheduler::conservative_buffered_points(500, None),
500
);
assert_eq!(
crate::scheduler::conservative_buffered_points(500, Some(250)),
250
);
assert_eq!(
crate::scheduler::conservative_buffered_points(500, Some(900)),
500
);
}
#[test]
fn test_frame_session_config_with_reconnect() {
let config = FrameSessionConfig::new(30_000)
.with_reconnect(crate::config::ReconnectConfig::new().max_retries(3));
assert!(config.reconnect.is_some());
assert_eq!(config.reconnect.as_ref().unwrap().max_retries, Some(3));
}
#[test]
fn test_frame_session_start_frame_session_rejects_invalid_pps_with_reconnect() {
use crate::backend::BackendKind;
use crate::device::{DacCapabilities, DacInfo, DacType, OutputModel};
use crate::stream::Dac;
let caps = DacCapabilities {
pps_min: 1000,
pps_max: 100_000,
max_points_per_chunk: 1000,
output_model: OutputModel::NetworkFifo,
};
struct MinimalBackend {
caps: DacCapabilities,
connected: bool,
}
impl crate::backend::DacBackend for MinimalBackend {
fn dac_type(&self) -> DacType {
DacType::Custom("Test".into())
}
fn caps(&self) -> &DacCapabilities {
&self.caps
}
fn connect(&mut self) -> crate::backend::Result<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> crate::backend::Result<()> {
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> crate::backend::Result<()> {
Ok(())
}
fn set_shutter(&mut self, _: bool) -> crate::backend::Result<()> {
Ok(())
}
}
impl crate::backend::FifoBackend for MinimalBackend {
fn try_write_points(
&mut self,
_: u32,
_: &[LaserPoint],
) -> crate::backend::Result<crate::backend::WriteOutcome> {
Ok(crate::backend::WriteOutcome::Written)
}
fn queued_points(&self) -> Option<u64> {
None
}
}
let backend = MinimalBackend {
caps: caps.clone(),
connected: false,
};
let info = DacInfo::new("test", "Test", DacType::Custom("Test".into()), caps);
let mut device = Dac::new(info, BackendKind::Fifo(Box::new(backend)));
device.reconnect_target = Some(crate::reconnect::ReconnectTarget {
device_id: "test".to_string(),
discovery_factory: None,
});
let config = FrameSessionConfig::new(500).with_reconnect(crate::config::ReconnectConfig::new());
let result = device.start_frame_session(config);
assert!(result.is_err());
}
#[test]
fn test_color_delay_line_resize_grow() {
let mut cdl = ColorDelayLine::new(1);
let mut points = vec![
LaserPoint::new(0.0, 0.0, 1, 1, 1, 1),
LaserPoint::new(0.0, 0.0, 2, 2, 2, 2),
LaserPoint::new(0.0, 0.0, 3, 3, 3, 3),
];
cdl.apply(&mut points);
cdl.resize(3);
assert_eq!(cdl.delay(), 3);
let mut points2 = vec![
LaserPoint::new(0.0, 0.0, 10, 10, 10, 10),
LaserPoint::new(0.0, 0.0, 20, 20, 20, 20),
LaserPoint::new(0.0, 0.0, 30, 30, 30, 30),
];
cdl.apply(&mut points2);
assert_eq!(
(
points2[0].r,
points2[0].g,
points2[0].b,
points2[0].intensity
),
(0, 0, 0, 0)
);
assert_eq!(
(
points2[1].r,
points2[1].g,
points2[1].b,
points2[1].intensity
),
(0, 0, 0, 0)
);
assert_eq!(
(
points2[2].r,
points2[2].g,
points2[2].b,
points2[2].intensity
),
(3, 3, 3, 3)
);
}
#[test]
fn test_color_delay_line_resize_shrink() {
let mut cdl = ColorDelayLine::new(3);
let mut points = vec![
LaserPoint::new(0.0, 0.0, 1, 1, 1, 1),
LaserPoint::new(0.0, 0.0, 2, 2, 2, 2),
LaserPoint::new(0.0, 0.0, 3, 3, 3, 3),
LaserPoint::new(0.0, 0.0, 4, 4, 4, 4),
];
cdl.apply(&mut points);
cdl.resize(1);
assert_eq!(cdl.delay(), 1);
let mut points2 = vec![
LaserPoint::new(0.0, 0.0, 10, 10, 10, 10),
LaserPoint::new(0.0, 0.0, 20, 20, 20, 20),
];
cdl.apply(&mut points2);
assert_eq!(
(
points2[0].r,
points2[0].g,
points2[0].b,
points2[0].intensity
),
(4, 4, 4, 4)
);
assert_eq!(
(
points2[1].r,
points2[1].g,
points2[1].b,
points2[1].intensity
),
(10, 10, 10, 10)
);
}
#[test]
fn test_color_delay_line_resize_to_zero() {
let mut cdl = ColorDelayLine::new(2);
let mut points = vec![
LaserPoint::new(0.0, 0.0, 1, 1, 1, 1),
LaserPoint::new(0.0, 0.0, 2, 2, 2, 2),
LaserPoint::new(0.0, 0.0, 3, 3, 3, 3),
];
cdl.apply(&mut points);
cdl.resize(0);
assert_eq!(cdl.delay(), 0);
let mut points2 = vec![
LaserPoint::new(0.0, 0.0, 10, 10, 10, 10),
LaserPoint::new(0.0, 0.0, 20, 20, 20, 20),
];
cdl.apply(&mut points2);
assert_eq!((points2[0].r, points2[0].g), (10, 10));
assert_eq!((points2[1].r, points2[1].g), (20, 20));
}
#[test]
fn test_color_delay_line_resize_same_is_noop() {
let mut cdl = ColorDelayLine::new(2);
let mut points = vec![
LaserPoint::new(0.0, 0.0, 1, 1, 1, 1),
LaserPoint::new(0.0, 0.0, 2, 2, 2, 2),
LaserPoint::new(0.0, 0.0, 3, 3, 3, 3),
];
cdl.apply(&mut points);
cdl.resize(2);
let mut points2 = vec![
LaserPoint::new(0.0, 0.0, 10, 10, 10, 10),
LaserPoint::new(0.0, 0.0, 20, 20, 20, 20),
LaserPoint::new(0.0, 0.0, 30, 30, 30, 30),
];
cdl.apply(&mut points2);
assert_eq!(
(
points2[0].r,
points2[0].g,
points2[0].b,
points2[0].intensity
),
(2, 2, 2, 2)
);
assert_eq!(
(
points2[1].r,
points2[1].g,
points2[1].b,
points2[1].intensity
),
(3, 3, 3, 3)
);
assert_eq!(
(
points2[2].r,
points2[2].g,
points2[2].b,
points2[2].intensity
),
(10, 10, 10, 10)
);
}
#[test]
fn test_color_delay_line_resize_from_zero() {
let mut cdl = ColorDelayLine::new(0);
let mut points = vec![LaserPoint::new(0.0, 0.0, 5, 5, 5, 5)];
cdl.apply(&mut points);
assert_eq!(points[0].r, 5);
cdl.resize(2);
assert_eq!(cdl.delay(), 2);
let mut points2 = vec![
LaserPoint::new(0.0, 0.0, 10, 10, 10, 10),
LaserPoint::new(0.0, 0.0, 20, 20, 20, 20),
LaserPoint::new(0.0, 0.0, 30, 30, 30, 30),
];
cdl.apply(&mut points2);
assert_eq!((points2[0].r, points2[0].g), (0, 0));
assert_eq!((points2[1].r, points2[1].g), (0, 0));
assert_eq!((points2[2].r, points2[2].g), (10, 10));
}