Verify: check diff pairs/buses crossing connections (swap, incomplete)

New pass 8 (core/domain/diff_check.{hpp,cpp}): every complete local diff
pair (X_P/X_N, name-based) resolves its legs to two bridged nets; peer
pairs on those nets must match leg for leg.
- DiffPolaritySwap: P legs meet N legs across a connection (sometimes
  intentional - reported for review), or both legs joined onto one net.
- DiffCrossIncomplete: pairs sharing only one leg, and diff-bus lanes
  crossing NOWHERE while sibling lanes cross (distributed/fan-out buses
  stay silent - validated against the real 7-card VPX system: 21 noisy
  findings down to 3 genuine dangling-lane reports, 0 false swaps).

diff_suffix/split_trailing_index/is_internal_name promoted out of
analysis.cpp's anonymous namespace for reuse. VerifyReport.diff_anomalies
wired into model_total() and all four renderers (TUI verify, script
engine, wx log, analyze Issues). 8 new test cases (466 assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 20:05:43 +02:00
parent 9cf43696a2
commit 0b10e1c1b7
11 changed files with 452 additions and 26 deletions

184
tests/test_diff_check.cpp Normal file
View File

@@ -0,0 +1,184 @@
#include <doctest/doctest.h>
#include "core/app/verify.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/connect.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"
// check_diff_crossings: a complete local diff pair (X_P / X_N) must cross a
// connection leg for leg. Both sides are judged by name only.
namespace {
// New pin on `part`, wired to (or creating) module signal `sig_name`.
Pin *wire(Module *m, Part *p, const std::string &pin_name,
const std::string &sig_name)
{
Pin *pin = new Pin(pin_name);
p->add(pin);
Signal *s = m->signals->merge(sig_name);
s->add(pin);
pin->connect(s);
return pin;
}
struct Rig {
System sys;
Module *a, *b;
Part *ja, *jb;
Rig() {
a = sys.modules()->merge("A");
b = sys.modules()->merge("B");
ja = new Part("J1"); a->add(ja);
jb = new Part("P1"); b->add(jb);
}
void bridge(std::initializer_list<std::pair<Pin *, Pin *>> wires) {
Connection *c = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
c->transform_name = "identity";
for (const auto &w : wires) c->pin_map.push_back(w);
sys.connections()->add(c);
}
};
} // namespace
TEST_CASE("diff crossing: straight P↔P / N↔N is silent") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
r.bridge({{ap, bp}, {an, bn}});
app::VerifyReport vr = app::verify(&r.sys);
CHECK(vr.diff_anomalies.empty());
}
TEST_CASE("diff crossing: swapped legs report ONE polarity-swap anomaly") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
r.bridge({{ap, bn}, {an, bp}}); // crossed on purpose
app::VerifyReport vr = app::verify(&r.sys);
REQUIRE(vr.diff_anomalies.size() == 1); // deduped: not once per side
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
CHECK(vr.diff_anomalies[0].message.find("polarity swapped")
!= std::string::npos);
}
TEST_CASE("diff crossing: a single bridged leg reports incomplete") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
wire(r.a, r.ja, "2", "TX_N"); // N leg stays local
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
wire(r.b, r.jb, "2", "RX_N"); // peer N leg exists, unbridged
r.bridge({{ap, bp}});
app::VerifyReport vr = app::verify(&r.sys);
REQUIRE(vr.diff_anomalies.size() == 1);
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffCrossIncomplete);
CHECK(vr.diff_anomalies[0].message.find("only the P legs")
!= std::string::npos);
}
TEST_CASE("diff crossing: an unsuffixed peer is not judged") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *b1 = wire(r.b, r.jb, "1", "RXP"); // no _P/_N suffix
Pin *b2 = wire(r.b, r.jb, "2", "RXN");
r.bridge({{ap, b2}, {an, b1}}); // even crossed: silent
app::VerifyReport vr = app::verify(&r.sys);
CHECK(vr.diff_anomalies.empty());
}
TEST_CASE("diff crossing: P and N joined onto one net is flagged") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *b1 = wire(r.b, r.jb, "1", "X");
Pin *b2 = wire(r.b, r.jb, "2", "X"); // same peer signal
r.bridge({{ap, b1}, {an, b2}});
app::VerifyReport vr = app::verify(&r.sys);
REQUIRE(vr.diff_anomalies.size() == 1);
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
CHECK(vr.diff_anomalies[0].message.find("join the same net")
!= std::string::npos);
}
TEST_CASE("diff bus crossing: a dangling lane is listed, once per side") {
Rig r;
// Two lanes on each side; only lane 0 is bridged — lane 1 crosses nowhere.
Pin *a0p = wire(r.a, r.ja, "1", "TX0_P");
Pin *a0n = wire(r.a, r.ja, "2", "TX0_N");
wire(r.a, r.ja, "3", "TX1_P");
wire(r.a, r.ja, "4", "TX1_N");
Pin *b0p = wire(r.b, r.jb, "1", "RX0_P");
Pin *b0n = wire(r.b, r.jb, "2", "RX0_N");
wire(r.b, r.jb, "3", "RX1_P");
wire(r.b, r.jb, "4", "RX1_N");
r.bridge({{a0p, b0p}, {a0n, b0n}});
app::VerifyReport vr = app::verify(&r.sys);
// One aggregated anomaly per side (each names its own lane signals).
REQUIRE(vr.diff_anomalies.size() == 2);
for (const auto &an : vr.diff_anomalies) {
CHECK(an.kind == AnomalyKind::DiffCrossIncomplete);
CHECK(an.message.find("lane(s) 1 do not cross") != std::string::npos);
}
}
TEST_CASE("diff bus crossing: a distributed bus (lanes fanned out) is silent") {
// A's two lanes go to two DIFFERENT modules — legitimate backplane fan-out.
System sys;
Module *a = sys.modules()->merge("A");
Module *b = sys.modules()->merge("B");
Module *c = sys.modules()->merge("C");
Part *ja = new Part("J1"); a->add(ja);
Part *jb = new Part("P1"); b->add(jb);
Part *jc = new Part("P1"); c->add(jc);
Pin *a0p = wire(a, ja, "1", "TX0_P");
Pin *a0n = wire(a, ja, "2", "TX0_N");
Pin *a1p = wire(a, ja, "3", "TX1_P");
Pin *a1n = wire(a, ja, "4", "TX1_N");
Pin *b0p = wire(b, jb, "1", "RX0_P");
Pin *b0n = wire(b, jb, "2", "RX0_N");
Pin *c0p = wire(c, jc, "1", "RX0_P");
Pin *c0n = wire(c, jc, "2", "RX0_N");
Connection *cb = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
cb->transform_name = "identity";
cb->pin_map.emplace_back(a0p, b0p);
cb->pin_map.emplace_back(a0n, b0n);
sys.connections()->add(cb);
Connection *cc = new Connection("A/J1 <-> C/P1", a, ja, c, jc);
cc->transform_name = "identity";
cc->pin_map.emplace_back(a1p, c0p);
cc->pin_map.emplace_back(a1n, c0n);
sys.connections()->add(cc);
app::VerifyReport vr = app::verify(&sys);
CHECK(vr.diff_anomalies.empty()); // every lane crosses somewhere
}
TEST_CASE("diff crossing: empty / unconnected systems are silent") {
System sys;
app::VerifyReport vr = app::verify(&sys);
CHECK(vr.diff_anomalies.empty());
// A pair that never crosses anything: silent (local pairs are fine).
Rig r;
wire(r.a, r.ja, "1", "TX_P");
wire(r.a, r.ja, "2", "TX_N");
vr = app::verify(&r.sys);
CHECK(vr.diff_anomalies.empty());
}