#include #include "core/domain/analysis.hpp" #include "core/domain/modules.hpp" #include "core/domain/parts.hpp" #include "core/domain/pins.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" #include #include namespace { // Add a signal of the given name to a module, plus a single pin on a // helper part so the signal is not stripped by `drop_singleton_signals` // when callers exercise that path. void add_signal(Module *mod, Part *prt, const std::string &sig_name) { Pin *pin = new Pin("p_" + sig_name); prt->add(pin); Signal *s = mod->signals->merge(sig_name); s->add(pin); pin->connect(s); } } // namespace TEST_CASE("analyze detects diff pairs and reports `_P` orphans only") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); add_signal(m, p, "GETH_DA_P"); add_signal(m, p, "GETH_DA_N"); add_signal(m, p, "PCIE_RX_P"); // orphan: _P only → reported add_signal(m, p, "USB_DM"); // not a diff pair (no _P/_N) add_signal(m, p, "lower_p"); add_signal(m, p, "lower_n"); // lowercase still matches add_signal(m, p, "RESET_N"); // _N only → NOT reported (active-low) add_signal(m, p, "BOOTMODE_N"); // _N only → NOT reported AnalysisReport r = analyze_system(sys.get()); int dp = 0, orphans = 0; for (const auto &g : r.groups) if (g.kind == GroupKind::DiffPair) ++dp; for (const auto &a : r.anomalies) if (a.kind == AnomalyKind::DiffPairOrphan) ++orphans; CHECK(dp == 2); // GETH_DA + lower CHECK(orphans == 1); // only PCIE_RX_P } TEST_CASE("analyze aggregates diff pairs into a diff bus by trailing index") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); add_signal(m, p, "MDI0_P"); add_signal(m, p, "MDI0_N"); add_signal(m, p, "MDI1_P"); add_signal(m, p, "MDI1_N"); add_signal(m, p, "MDI2_P"); add_signal(m, p, "MDI2_N"); add_signal(m, p, "MDI3_P"); add_signal(m, p, "MDI3_N"); // Underscore-separated index variant. add_signal(m, p, "PCIE_TX_0_P"); add_signal(m, p, "PCIE_TX_0_N"); add_signal(m, p, "PCIE_TX_1_P"); add_signal(m, p, "PCIE_TX_1_N"); // A lonely pair under a bus-able stem (only one index) must stay DiffPair. add_signal(m, p, "USB3_TX_P"); add_signal(m, p, "USB3_TX_N"); AnalysisReport r = analyze_system(sys.get()); int dp = 0, db = 0; bool mdi_found = false, pcie_found = false; for (const auto &g : r.groups) { if (g.kind == GroupKind::DiffPair) ++dp; if (g.kind == GroupKind::DiffBus) ++db; if (g.kind == GroupKind::DiffBus && g.label.find("MDI[") == 0) { mdi_found = true; CHECK(g.lo == 0); CHECK(g.hi == 3); CHECK(g.members.size() == 8); // 4 pairs × 2 signals } if (g.kind == GroupKind::DiffBus && g.label.find("PCIE_TX_[") == 0) { pcie_found = true; CHECK(g.lo == 0); CHECK(g.hi == 1); CHECK(g.members.size() == 4); } } CHECK(db == 2); CHECK(dp == 1); // USB3_TX kept as solo DiffPair (single index — degenerate bus) CHECK(mdi_found); CHECK(pcie_found); } TEST_CASE("analyze flags a diff bus with a missing lane") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); add_signal(m, p, "LANE0_P"); add_signal(m, p, "LANE0_N"); add_signal(m, p, "LANE1_P"); add_signal(m, p, "LANE1_N"); // LANE2 missing add_signal(m, p, "LANE3_P"); add_signal(m, p, "LANE3_N"); AnalysisReport r = analyze_system(sys.get()); int gaps = 0; for (const auto &a : r.anomalies) if (a.kind == AnomalyKind::DiffBusGap) ++gaps; CHECK(gaps == 1); } TEST_CASE("analyze detects buses with bracketed and underscore forms") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); add_signal(m, p, "DATA[0]"); add_signal(m, p, "DATA[1]"); add_signal(m, p, "DATA[2]"); add_signal(m, p, "ADDR_0"); add_signal(m, p, "ADDR_1"); add_signal(m, p, "STANDALONE"); AnalysisReport r = analyze_system(sys.get()); int buses = 0; bool data_found = false, addr_found = false; for (const auto &g : r.groups) { if (g.kind != GroupKind::Bus) continue; ++buses; if (g.label.find("DATA") == 0) { data_found = true; CHECK(g.lo == 0); CHECK(g.hi == 2); CHECK(g.members.size() == 3); } else if (g.label.find("ADDR_") == 0) { addr_found = true; CHECK(g.lo == 0); CHECK(g.hi == 1); CHECK(g.members.size() == 2); } } CHECK(buses == 2); CHECK(data_found); CHECK(addr_found); } TEST_CASE("analyze flags bus gaps") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); add_signal(m, p, "D[0]"); add_signal(m, p, "D[1]"); add_signal(m, p, "D[3]"); // gap at 2 add_signal(m, p, "D[5]"); // gap at 4 AnalysisReport r = analyze_system(sys.get()); int gaps = 0; for (const auto &a : r.anomalies) if (a.kind == AnomalyKind::BusGap) ++gaps; CHECK(gaps == 1); // one bus, one anomaly listing all missing indices } TEST_CASE("analyze ignores single-suffix \"buses\" and pure numbers") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); add_signal(m, p, "CLK_100MHZ"); // not a bus (single signal) add_signal(m, p, "GND"); // no digit suffix AnalysisReport r = analyze_system(sys.get()); for (const auto &g : r.groups) CHECK(g.kind != GroupKind::Bus); } TEST_CASE("analyze rejects digits not preceded by `_` (false-positive guard)") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); // GETH_01_VDD12, GETH_01_VDD13 look like a bus with stem "GETH_01_VDD" // but there is no underscore before the digits — must NOT be detected. add_signal(m, p, "GETH_01_VDD12"); add_signal(m, p, "GETH_01_VDD13"); add_signal(m, p, "GETH_01_VDD14"); AnalysisReport r = analyze_system(sys.get()); for (const auto &g : r.groups) CHECK(g.kind != GroupKind::Bus); } TEST_CASE("analyze skips internal `$xxx` Mentor names") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); add_signal(m, p, "$N123"); add_signal(m, p, "$N124"); add_signal(m, p, "$N125"); add_signal(m, p, "$SIG_P"); add_signal(m, p, "$SIG_N"); AnalysisReport r = analyze_system(sys.get()); CHECK(r.groups.empty()); CHECK(r.anomalies.empty()); } TEST_CASE("analyze on empty / null system") { AnalysisReport r = analyze_system(nullptr); CHECK(r.groups.empty()); CHECK(r.anomalies.empty()); auto sys = std::make_unique(); r = analyze_system(sys.get()); CHECK(r.groups.empty()); CHECK(r.anomalies.empty()); } TEST_CASE("infer_signal_types: Power requires name+structural agreement") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); auto fan_out = [&](Signal *s, int n) { for (int i = 0; i < n; ++i) { Pin *pin = new Pin(s->name + "_" + std::to_string(i)); p->add(pin); s->add(pin); pin->connect(s); } }; Signal *p_3v3 = m->signals->merge("PWR_3V3"); fan_out(p_3v3, 3); // voltage + ≥ floor → Power Signal *vcc = m->signals->merge("VCC"); fan_out(vcc, 5); // fan-out ≥ 4 → Power Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // < 3 → hard floor → Other Signal *pwr_2 = m->signals->merge("PWR_2"); fan_out(pwr_2, 2); // < 3 → hard floor → Other Signal *gnd = m->signals->merge("GND"); fan_out(gnd, 1); // gnd: name alone Signal *clk = m->signals->merge("CLK_50MHZ"); fan_out(clk, 3); // not power-ish → Other auto st = infer_signal_types(sys.get()); CHECK(st.power == 2); // PWR_3V3, VCC CHECK(st.gnd == 1); // GND (name alone) CHECK(st.kept_other == 2); // PWR_OK, PWR_2 below the hard floor CHECK(p_3v3->type == SignalType::Power); CHECK(vcc->type == SignalType::Power); CHECK(gnd->type == SignalType::GndShield); CHECK(pwr_ok->type == SignalType::Other); CHECK(pwr_2->type == SignalType::Other); CHECK(clk->type == SignalType::Other); } TEST_CASE("infer_signal_types: fan-out hard floor overrides voltage in name") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); Part *p = new Part("U1"); m->add(p); // VS_3V3 has a voltage pattern that would normally confirm Power, but // with only 2 pins it must still drop to Other because of the hard floor. Signal *s = m->signals->merge("VS_3V3"); Pin *p1 = new Pin("p1"); p->add(p1); s->add(p1); p1->connect(s); Pin *p2 = new Pin("p2"); p->add(p2); s->add(p2); p2->connect(s); auto st = infer_signal_types(sys.get()); CHECK(st.power == 0); CHECK(st.kept_other == 1); CHECK(s->type == SignalType::Other); } TEST_CASE("Signal ctor defaults type to Other (no auto-inference)") { Signal s("VCC"); CHECK(s.type == SignalType::Other); } TEST_CASE("analyze scopes detection per module (no cross-module merge)") { auto sys = std::make_unique(); Module *m1 = sys->modules()->merge("M1"); Module *m2 = sys->modules()->merge("M2"); Part *p1 = new Part("U1"); m1->add(p1); Part *p2 = new Part("U1"); m2->add(p2); add_signal(m1, p1, "SIG_P"); add_signal(m2, p2, "SIG_N"); // matching half lives in another module AnalysisReport r = analyze_system(sys.get()); int dp = 0, orphans = 0; for (const auto &g : r.groups) if (g.kind == GroupKind::DiffPair) ++dp; for (const auto &a : r.anomalies) if (a.kind == AnomalyKind::DiffPairOrphan) ++orphans; CHECK(dp == 0); CHECK(orphans == 1); // only the `_P` side is reported (active-low rule) }