Files
essim/tests/test_analysis.cpp
François 1943f1f88a Power inference: classify rail+control names as power-adjacent, not suspect
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>
2026-06-04 16:41:19 +02:00

320 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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)
}