Names holding both a rail token (VCC/VDD/PWR/...) and a control token (SENSE, EN, PG, FB, OK, FAULT, ...) are signals ABOUT a rail - feedback, enable, power-good - so their non-Power classification is confident. They used to land in the Suspect bucket, drowning the genuine ambiguities. - classify_signal_name(): 3-state name verdict (Rail / PowerAdjacent / GndShield / Other) with whole-token matching (trailing digits stripped, long lexemes also match as suffix: VSENSE, PWRGOOD, NFAULT). infer_signal_type() becomes a thin wrapper, so the dashboard suspect count and the export suspect column shrink automatically. - infer_signal_types(): PowerAdjacent -> Other + new `adjacent` stat, before the structural gate (a big-fanout sense net stays Other). - LoadResult.adjacent rendered by all three consumers (TUI command, script engine, wx log) - outputs kept in sync. - analyze Types tab: new [Pwr-adjacent] rows with the deciding token, deliberate sort order (Power, Suspect, Adjacent, Gnd), glossary entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
320 lines
11 KiB
C++
320 lines
11 KiB
C++
#include <doctest/doctest.h>
|
||
|
||
#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 <memory>
|
||
#include <string>
|
||
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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<System>();
|
||
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); // control token → adjacent
|
||
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 == 1); // PWR_2 below the hard floor
|
||
CHECK(st.adjacent == 1); // PWR_OK: power-good control, not suspect
|
||
|
||
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: power-adjacent beats fan-out — a big sense net is still Other") {
|
||
auto sys = std::make_unique<System>();
|
||
Module *m = sys->modules()->merge("M");
|
||
Part *p = new Part("U1"); m->add(p);
|
||
|
||
// VDD_CORE_SENSE with fan-out 5: structure alone would confirm Power,
|
||
// but the control token settles it as a measurement net → Other,
|
||
// counted adjacent (not suspect, not power).
|
||
Signal *s = m->signals->merge("VDD_CORE_SENSE");
|
||
for (int i = 0; i < 5; ++i) {
|
||
Pin *pin = new Pin("p" + std::to_string(i));
|
||
p->add(pin); s->add(pin); pin->connect(s);
|
||
}
|
||
|
||
auto st = infer_signal_types(sys.get());
|
||
CHECK(st.power == 0);
|
||
CHECK(st.kept_other == 0);
|
||
CHECK(st.adjacent == 1);
|
||
CHECK(s->type == SignalType::Other);
|
||
}
|
||
|
||
TEST_CASE("infer_signal_types: fan-out hard floor overrides voltage in name") {
|
||
auto sys = std::make_unique<System>();
|
||
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<System>();
|
||
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)
|
||
}
|