fusible 0.20.1

Electronic linter: a fuse that blows to block firmware code generation
use super::place::{apply, placement, IDENTITY};
use super::probe::{probes, Probe, Role, Shape, Side};
use crate::commands::body::build_kit;
use crate::config::PlacementConfig;

pub(super) const BOARD: &str = r#"[
  {"type":"pcb_board","width":40,"height":30,"center":{"x":0,"y":0},"thickness":1.4},
  {"type":"pcb_hole","x":-15,"y":10,"hole_diameter":3.2},
  {"type":"source_component","source_component_id":"sc1","name":"J1"},
  {"type":"pcb_component","pcb_component_id":"pc1","source_component_id":"sc1",
   "center":{"x":18,"y":0},"width":6,"height":8,"layer":"top"},
  {"type":"cad_component","pcb_component_id":"pc1","position":{"x":18,"y":0,"z":3.5},"size":{"x":6,"y":8,"z":4.0}}
]"#;

pub(super) fn near_eq(a: f64, b: f64) -> bool {
    (a - b).abs() < 1e-9
}

pub(super) fn built(frame: &[f64; 12]) -> Vec<Probe> {
    let kit = build_kit(BOARD, Some("main".into())).unwrap();
    probes("main", &kit, frame)
}

#[test]
fn a_board_with_one_hole_and_one_connector_gets_every_probe_kind() {
    let all = built(&IDENTITY);
    let count = |f: fn(&Role) -> bool| all.iter().filter(|p| f(&p.role)).count();
    assert_eq!(all.len(), 1 + 1 + 1 + 2 * 5 + 1);
    assert_eq!(count(|r| matches!(r, Role::Slab)), 1);
    assert_eq!(count(|r| matches!(r, Role::KeepOut { .. })), 1);
    assert_eq!(count(|r| matches!(r, Role::Opening { .. })), 1);
    assert_eq!(count(|r| matches!(r, Role::HoleNear { .. })), 2);
    assert_eq!(count(|r| matches!(r, Role::HoleQuadrant { .. })), 8);
    let mut ids: Vec<&str> = all.iter().map(|p| p.id.as_str()).collect();
    ids.sort_unstable();
    ids.dedup();
    assert_eq!(ids.len(), all.len());
    let value = serde_json::to_value(&all[0]).unwrap();
    assert_eq!(value["kind"], "prism");
    assert_eq!(value["show"], true);
    assert_eq!(value["matrix"].as_array().unwrap().len(), 12);
}

#[test]
fn the_slab_shrinks_by_the_inset_and_follows_the_placement() {
    let turn = placement(&PlacementConfig {
        at: Some([10.0, 0.0, 0.0]),
        rotate: Some([0.0, 0.0, 90.0]),
    });
    let all = built(&turn);
    let slab = &all[0];
    let Shape::Prism { points, z0, z1 } = &slab.shape else {
        panic!("the slab is a prism: {:?}", slab.shape);
    };
    assert!(near_eq(*z0, -0.6) && near_eq(*z1, 0.6));
    let mut placed: Vec<[f64; 2]> = points
        .iter()
        .map(|p| {
            let q = apply(&slab.matrix, [p[0], p[1], 0.0]);
            [q[0], q[1]]
        })
        .collect();
    placed.sort_by(|a, b| a.partial_cmp(b).unwrap());
    let expected = [[-4.9, -19.9], [-4.9, 19.9], [24.9, -19.9], [24.9, 19.9]];
    for (got, want) in placed.iter().zip(expected) {
        assert!(
            near_eq(got[0], want[0]) && near_eq(got[1], want[1]),
            "{placed:?}"
        );
    }
}

#[test]
fn the_opening_probe_reaches_outward_from_the_edge_over_the_connector_height() {
    let all = built(&IDENTITY);
    let opening = all
        .iter()
        .find(|p| matches!(p.role, Role::Opening { .. }))
        .unwrap();
    let Shape::Prism { points, z0, z1 } = &opening.shape else {
        panic!("the opening is a prism");
    };
    assert!(near_eq(*z0, 1.6) && near_eq(*z1, 5.4), "{z0} {z1}");
    let xs: Vec<f64> = points.iter().map(|p| p[0]).collect();
    let ys: Vec<f64> = points.iter().map(|p| p[1]).collect();
    assert!(near_eq(xs.iter().cloned().fold(f64::MAX, f64::min), 20.1));
    assert!(near_eq(xs.iter().cloned().fold(f64::MIN, f64::max), 22.9));
    assert!(near_eq(ys.iter().cloned().fold(f64::MAX, f64::min), -4.4));
    assert!(near_eq(ys.iter().cloned().fold(f64::MIN, f64::max), 4.4));
    assert!(opening.collide && !opening.show);
}

#[test]
fn the_near_probe_is_a_wide_cylinder_clipped_to_the_outline() {
    let all = built(&IDENTITY);
    let near = all
        .iter()
        .find(|p| {
            matches!(
                p.role,
                Role::HoleNear {
                    side: Side::Top,
                    ..
                }
            )
        })
        .unwrap();
    let Shape::Cylinder {
        center,
        r,
        z0,
        z1,
        clip,
    } = &near.shape
    else {
        panic!("the near probe is a cylinder");
    };
    assert_eq!(*center, [-15.0, 10.0]);
    assert!(near_eq(*r, 1.6 + 4.0 - 0.1));
    assert!(near_eq(*z0, 0.8) && near_eq(*z1, 2.6), "{z0} {z1}");
    assert_eq!(clip.as_ref().map(Vec::len), Some(4));
    assert!(!near.collide);
}

#[test]
fn every_hole_face_gets_four_ring_quadrants() {
    let rings: Vec<u8> = built(&IDENTITY)
        .iter()
        .filter_map(|p| match p.shape {
            Shape::Ring { quadrant, .. } => Some(quadrant),
            _ => None,
        })
        .collect();
    assert_eq!(rings, [0, 1, 2, 3, 0, 1, 2, 3]);
}

#[test]
fn a_connector_without_a_model_height_is_assumed_two_millimetres_tall() {
    let bare = BOARD.replace(r#""size":{"x":6,"y":8,"z":4.0}"#, r#""size":{}"#);
    let kit = build_kit(&bare, None).unwrap();
    let all = probes("main", &kit, &IDENTITY);
    let opening = all
        .iter()
        .find(|p| matches!(p.role, Role::Opening { .. }))
        .unwrap();
    let Shape::Prism { z0, z1, .. } = &opening.shape else {
        panic!("the opening is a prism");
    };
    assert!(near_eq(*z0, 0.8) && near_eq(*z1, 2.6), "{z0} {z1}");
}