fusible 0.20.8

Electronic linter: a fuse that blows to block firmware code generation
use super::eject_manifest::eject_manifest;
use crate::config::FusibleConfig;

const PATH: &str = "bodies/lid.step";

fn authored(text: &str, name: &str) -> (Option<String>, Option<String>) {
    let body = &FusibleConfig::from_str(text).unwrap().bodies[name];
    let source = body.source.as_ref().map(|p| p.display().to_string());
    (source, body.generate.clone())
}

const BLOCK: &str = "\
name: rover  # the project
boards:
  main: { entry: boards/main.json }
  sensor: { entry: boards/sensor.json }

# physical parts
bodies:
  case:
    source: bodies/case.step
  lid:
    # generated for the sensor
    generate: enclosure
    board: sensor
    wall_thickness: 1.5
    format: all
    ventilation: grid  # airflow

  knob: { source: bodies/knob.step }
firmware:
  fw: { board: main }
";

#[test]
fn block_entry_becomes_authored_and_keeps_comments() {
    let out = eject_manifest(BLOCK, "lid", PATH).unwrap();
    assert_eq!(authored(&out, "lid"), (Some(PATH.into()), None));
    for kept in [
        "name: rover  # the project",
        "# physical parts",
        "    # generated for the sensor",
        "    board: sensor",
        "  knob: { source: bodies/knob.step }",
        "firmware:",
    ] {
        assert!(out.contains(kept), "lost `{kept}` in:\n{out}");
    }
    assert!(!out.contains("wall_thickness") && !out.contains("airflow"));
    assert_eq!(out.matches("source:").count(), 3);
    let sensor = FusibleConfig::from_str(&out).unwrap().bodies["lid"]
        .board
        .clone();
    assert_eq!(sensor.as_deref(), Some("sensor"));
}

#[test]
fn flow_entry_becomes_authored_with_its_board_and_comment() {
    let text = concat!(
        "boards: {a: {}, b: {}}\n",
        "bodies:\n",
        "  lid: { generate: enclosure, board: b, wall_thickness: 1.5 }  # keep me\n",
        "  x: { source: x.step }\n",
    );
    let out = eject_manifest(text, "lid", PATH).unwrap();
    assert!(
        out.contains("  lid: { source: bodies/lid.step, board: b }  # keep me"),
        "{out}"
    );
    assert_eq!(authored(&out, "lid"), (Some(PATH.into()), None));
    assert!(out.contains("  x: { source: x.step }"));
}

#[test]
fn legacy_mechanical_block_moves_into_a_bodies_section() {
    let text = concat!(
        "name: fieldcam\nhardware:\n  entry: hardware/index.tsx\n\n",
        "mechanical:\n  format: all\n  wall_thickness: 2.5\n\n",
        "firmware:\n  path: apps/firmware\n",
    );
    let out = eject_manifest(text, "fieldcam", "bodies/enclosure.step").unwrap();
    assert!(!out.contains("mechanical:") && !out.contains("wall_thickness"));
    assert!(!out.contains("\n\n\n"), "{out}");
    assert!(
        out.ends_with("\nbodies:\n  fieldcam:\n    source: bodies/enclosure.step\n"),
        "{out}"
    );
    assert_eq!(
        authored(&out, "fieldcam").0.as_deref(),
        Some("bodies/enclosure.step")
    );
    assert!(out.contains("firmware:\n  path: apps/firmware"));
}

#[test]
fn legacy_one_line_mechanical_is_replaced() {
    let text = "name: p\nmechanical: { height: 20 }\n";
    let out = eject_manifest(text, "p", PATH).unwrap();
    assert_eq!(
        out,
        "name: p\n\nbodies:\n  p:\n    source: bodies/lid.step\n"
    );
}

#[test]
fn odd_paths_are_quoted() {
    let out = eject_manifest(BLOCK, "lid", "my parts/lid, v2.step").unwrap();
    assert!(
        out.contains("    source: \"my parts/lid, v2.step\""),
        "{out}"
    );
    assert_eq!(
        authored(&out, "lid").0.as_deref(),
        Some("my parts/lid, v2.step")
    );
}

#[test]
fn unsafe_shapes_are_refused_and_nothing_is_returned() {
    let multi = "bodies:\n  lid:\n    generate: enclosure\n    wall_thickness:\n      3.0\n";
    let quoted = "bodies:\n  lid: { generate: \"enclosure\" }\n";
    let nested = "bodies:\n  lid: { generate: enclosure,\n    height: 20 }\n";
    let crlf = "bodies:\r\n  lid:\r\n    generate: enclosure\r\n";
    let missing = "boards: {a: {}}\n";
    let no_entry = "bodies:\n  other: { source: o.step }\n";
    for (text, why) in [
        (multi, "several lines"),
        (quoted, "quotes"),
        (nested, "several lines"),
        (crlf, "CRLF"),
        (missing, "neither"),
        (no_entry, "not in"),
    ] {
        let err = eject_manifest(text, "lid", PATH).unwrap_err();
        assert!(err.contains(why), "`{err}` should mention `{why}`");
    }
}

#[test]
fn an_authored_body_cannot_be_ejected_again() {
    let err = eject_manifest(BLOCK, "case", PATH).unwrap_err();
    assert!(err.contains("already has a `source:`"), "{err}");
}