Files
essim/tests/test_analysis.cpp
François 9cf43696a2 Rename the power-adjacent category to "power management"
"Adjacent" read as jargon; "power management" is the standard EE umbrella
for enable/power-good/sense/fault/seq signals (cf. PMIC). Renamed across
the board: NameVerdict::PowerMgmt, stats/LoadResult field `mgmt`, analyze
tag [Power mgmt] + header "Pwr-mgmt" + glossary, load lines now say
"power-management (control/measure — kept as Other)" (TUI / script / wx
kept in sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:21:25 +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 → pwr-mgmt
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.mgmt == 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-management 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 mgmt (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.mgmt == 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)
}