- 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>
126 lines
4.2 KiB
C++
126 lines
4.2 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "system/modules.hpp"
|
|
#include "system/parts.hpp"
|
|
#include "system/persist.hpp"
|
|
#include "system/pins.hpp"
|
|
#include "system/signals.hpp"
|
|
#include "system/system.hpp"
|
|
|
|
#include <filesystem>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
TEST_CASE("nc_origin_tag round-trips with from_tag for tagged variants") {
|
|
NcOrigin o;
|
|
REQUIRE(nc_origin_from_tag("U", o));
|
|
CHECK(o == NcOrigin::ImportedUnconnected);
|
|
REQUIRE(nc_origin_from_tag("D", o));
|
|
CHECK(o == NcOrigin::DroppedSingleton);
|
|
|
|
CHECK(std::string(nc_origin_tag(NcOrigin::ImportedUnconnected)) == "U");
|
|
CHECK(std::string(nc_origin_tag(NcOrigin::DroppedSingleton)) == "D");
|
|
CHECK(std::string(nc_origin_tag(NcOrigin::None)) == "");
|
|
}
|
|
|
|
TEST_CASE("nc_origin_from_tag rejects unknown / empty tags") {
|
|
NcOrigin o = NcOrigin::None;
|
|
CHECK(!nc_origin_from_tag("", o));
|
|
CHECK(!nc_origin_from_tag("X", o));
|
|
CHECK(!nc_origin_from_tag("Unknown", o));
|
|
}
|
|
|
|
TEST_CASE("drop_singleton_signals detaches size-1 signals and tags pins") {
|
|
Module mod("M");
|
|
Part *prt = new Part("U1");
|
|
mod.add(prt);
|
|
|
|
auto add_pin = [&](const std::string &pin_name, const std::string &sig) {
|
|
Pin *p = new Pin(pin_name);
|
|
prt->add(p);
|
|
Signal *s = mod.signals->merge(sig);
|
|
s->add(p);
|
|
p->connect(s);
|
|
return p;
|
|
};
|
|
|
|
// Two-pin signal: should survive.
|
|
Pin *pa = add_pin("A1", "BUS_X");
|
|
Pin *pb = add_pin("A2", "BUS_X");
|
|
// Singleton: should be dropped, pin tagged DroppedSingleton.
|
|
Pin *po = add_pin("A3", "ORPHAN");
|
|
|
|
REQUIRE(mod.signals->size() == 2);
|
|
int dropped = drop_singleton_signals(mod.signals);
|
|
CHECK(dropped == 1);
|
|
CHECK(mod.signals->size() == 1); // BUS_X kept
|
|
CHECK(mod.signals->exists("BUS_X"));
|
|
CHECK(!mod.signals->exists("ORPHAN"));
|
|
|
|
CHECK(pa->signal() != nullptr); // unchanged
|
|
CHECK(pb->signal() != nullptr);
|
|
CHECK(po->signal() == nullptr); // detached
|
|
CHECK(po->nc_origin == NcOrigin::DroppedSingleton);
|
|
CHECK(pa->nc_origin == NcOrigin::None);
|
|
}
|
|
|
|
TEST_CASE("drop_singleton_signals is a no-op on a clean module") {
|
|
Module mod("M");
|
|
Part *prt = new Part("U1");
|
|
mod.add(prt);
|
|
auto *p1 = new Pin("A1"); prt->add(p1);
|
|
auto *p2 = new Pin("A2"); prt->add(p2);
|
|
Signal *s = mod.signals->merge("MULTIPIN");
|
|
s->add(p1); p1->connect(s);
|
|
s->add(p2); p2->connect(s);
|
|
|
|
CHECK(drop_singleton_signals(mod.signals) == 0);
|
|
CHECK(mod.signals->size() == 1);
|
|
}
|
|
|
|
TEST_CASE("persist round-trip preserves nc_origin tags") {
|
|
auto sys = std::make_unique<System>();
|
|
Module *mod = sys->modules()->merge("M");
|
|
Part *prt = new Part("U1");
|
|
mod->add(prt);
|
|
|
|
auto *connected = new Pin("A1");
|
|
prt->add(connected);
|
|
Signal *s = mod->signals->merge("SIG");
|
|
s->add(connected); connected->connect(s);
|
|
|
|
auto *imported_nc = new Pin("A2");
|
|
imported_nc->nc_origin = NcOrigin::ImportedUnconnected;
|
|
prt->add(imported_nc);
|
|
|
|
auto *dropped = new Pin("A3");
|
|
dropped->nc_origin = NcOrigin::DroppedSingleton;
|
|
prt->add(dropped);
|
|
|
|
// A pin with no signal and no origin (e.g. an old snapshot or a
|
|
// FillIdentityNCs-style materialisation): tag stays None on restore.
|
|
auto *bare_nc = new Pin("A4");
|
|
prt->add(bare_nc);
|
|
|
|
std::string path =
|
|
(std::filesystem::temp_directory_path() / "essim_nc_origin.txt").string();
|
|
std::string err;
|
|
REQUIRE(save_system(sys.get(), path, err));
|
|
|
|
std::unique_ptr<System> restored(restore_system(path, err));
|
|
REQUIRE(restored);
|
|
std::filesystem::remove(path);
|
|
|
|
Part *rp = restored->modules()->get("M")->get("U1");
|
|
CHECK(rp->get("A1")->nc_origin == NcOrigin::None);
|
|
CHECK(rp->get("A2")->nc_origin == NcOrigin::ImportedUnconnected);
|
|
CHECK(rp->get("A3")->nc_origin == NcOrigin::DroppedSingleton);
|
|
CHECK(rp->get("A4")->nc_origin == NcOrigin::None);
|
|
}
|
|
|
|
TEST_CASE("next_signal_type cycles Power → Gnd → Other → Power") {
|
|
CHECK(next_signal_type(SignalType::Power) == SignalType::GndShield);
|
|
CHECK(next_signal_type(SignalType::GndShield) == SignalType::Other);
|
|
CHECK(next_signal_type(SignalType::Other) == SignalType::Power);
|
|
}
|