- New `src/system/analysis.{hpp,cpp}` — stateless post-processing pass
`analyze_system(System*) → AnalysisReport`. Per-module detection of
signal groups and anomalies; pure read, re-runnable.
- Groups: diff pairs (`*_P` / `*_N`, case-insensitive), buses
(`NAME[N]` or strict `NAME_N` — the `_` before digits is required
so names like `GETH_01_VDD12` are not misread as a bus).
- Anomalies: `DiffPairOrphan` (asymmetric: only `_P` without `_N` is
reported — `_N` alone is overloaded with active-low semantics and
floods the output with false positives), `BusGap` (missing index
inside a detected `[lo..hi]`).
- Noise filters: signals starting with `$` (Mentor internals) are
skipped wholesale.
- New `analyze` shell command — prints groups sorted by module +
label, then anomalies. Sized for the upcoming dashboard.
- `tests/test_analysis.cpp` — 8 cases covering both detectors, false-
positive guards (no-underscore digits, `$`-prefixed internals), and
per-module scoping.
- `tests/test_nc_origin.cpp` — completes the prior NC-tagging commit
with round-trip + drop_singleton_signals coverage.
- DESIGN.md updated: layout entry for `analysis.{hpp,cpp}` and new
section explaining the pass; NC-origin paragraph aligned with the
actual tag semantics and the verify three-pass summary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
183 lines
5.8 KiB
C++
183 lines
5.8 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "system/analysis.hpp"
|
|
#include "system/modules.hpp"
|
|
#include "system/parts.hpp"
|
|
#include "system/pins.hpp"
|
|
#include "system/signals.hpp"
|
|
#include "system/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 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("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)
|
|
}
|