main
Colby Knox standard 0.15.2: engine 0.12.2, assembly_check one-land pads and pad-less parts (#24), plan_route withoutAvoid (#21) 275a6ef 12d ago
//! The engine against a bridge's own answers on the same boards. Fixtures are not kept with the standard: each bridge's
//! page carries its own (Altium's: lane and the Bluetooth Sentinel, from altium-lane, bridge 0.2.76). Point
//! ADOM_EDA_FIXTURES at a folder holding `altium/<board>.<model>.json` and `altium/expected/<board>.<check>.json`;
//! without it these tests are skipped. The result keys must match exactly; hints and notes may differ, since the engine
//! names verbs without a prefix.
use serde_json::{json, Value};
use std::path::PathBuf;

fn dir() -> Option<PathBuf> {
    let d = PathBuf::from(std::env::var_os("ADOM_EDA_FIXTURES")?).join("altium");
    d.is_dir().then_some(d)
}

fn load(p: PathBuf) -> Option<Value> { Some(serde_json::from_str(&std::fs::read_to_string(p).ok()?).unwrap()) }

fn bundle(d: &PathBuf, board: &str) -> Option<Value> {
    Some(json!({"board": load(d.join(format!("{board}.routing_state.json")))?, "stack": load(d.join(format!("{board}.layer_stack.json")))?,
                "pours": load(d.join(format!("{board}.pour_state.json")))?, "rules": load(d.join(format!("{board}.read_rules.json"))),
                "silk": load(d.join(format!("{board}.silk_state.json"))), "bodies": load(d.join(format!("{board}.bodies.json"))), "args": {}}))
}

fn same(check: &str, keys: &[&str]) {
    let Some(d) = dir() else { eprintln!("skipped: set ADOM_EDA_FIXTURES to a fixtures folder to compare {check} with a bridge"); return };
    let mut ran = 0;
    for board in ["lane", "sentinel"] {
        let (Some(input), Some(want)) = (bundle(&d, board), load(d.join(format!("expected/{board}.{check}.json")))) else { continue };
        let got = adom_eda_checks::run(check, &input);
        assert_eq!(got["success"], json!(true), "{board} {check}: {got}");
        for k in keys {
            let ptr = format!("/{}", k.replace('.', "/"));
            let optional = k.starts_with("measured.") || (k.starts_with("fix.") && want["fix"].is_null());
            assert!(want.pointer(&ptr).is_some() || optional, "{board} {check}: the Altium reply has no {k}");
            assert_eq!(got.pointer(&ptr), want.pointer(&ptr), "{board} {check}: {k} differs from the Altium bridge");
        }
        ran += 1;
    }
    assert!(ran > 0, "no fixtures for {check} in {}", d.display());
}

#[test]
fn copper_balance_matches_altium() {
    same("copper_balance", &["boardMm2", "resolutionMm", "layers", "pairs", "balanced", "poursWithoutRegions"]);
}

#[test]
fn return_path_matches_altium() {
    same("return_path", &["referenceNets", "layersWithNoReferenceCopper", "nets", "ok", "resolutionMm",
                          "fix.args", "fix.patches", "fix.closes", "fix.floating"]);
}

#[test]
fn fab_check_measures_what_altium_measures() {
    same("fab_check", &["measured.boardSize", "measured.clearanceRule", "measured.copperLayers", "measured.counts", "measured.edgeClearance", "measured.holeToHolePad",
                        "measured.holeToHoleVia", "measured.largestDrill", "measured.padHole", "measured.smdPad", "measured.track",
                        "measured.viaAnnularRing", "measured.viaDiameter", "measured.viaDrill", "outsideLimits", "limitsChecked"]);
}

#[test]
fn height_check_matches_altium() {
    same("height_check", &["ok", "complete", "bodies", "parts", "tallest", "overLimit", "regionHits", "collisions", "tooClose", "minGapMm", "noBody", "heights"]);
}

#[test]
fn placement_check_matches_altium() {
    same("placement_check", &["ok", "parts", "courtyardOverlaps", "offOrOverEdge", "onMountingHoles", "nearEdge", "marginMm", "edgeMm"]);
}

#[test]
fn assembly_check_matches_altium() {
    // board.parts: the engine counts only components with pads (a logo is no part, fusion-lane #24), Altium all of them
    same("assembly_check", &["service", "fab", "board.bottomSideParts", "board.finestPitch", "board.smallestChip", "board.tinyFootprints", "board.fiducials", "board.throughHoleParts", "economic.ok", "standard.ok", "economic.rules.0.ok", "economic.rules.1.ok", "economic.rules.2.ok",
                             "economic.rules.3.ok", "economic.rules.4.ok", "economic.rules.5.ok", "standard.rules.0.ok", "standard.rules.3.ok"]);
}