Extract verify into core (app::verify); thin the TUI command.

Move the 7-pass verify orchestration out of the TUI command lambda and
into core/app/verify.{hpp,cpp}: app::verify(System*) returns a structured
VerifyReport (role mismatches, net inconsistencies, orphan counts, the four
model-driven anomaly vectors) with no Print/dialog/FTXUI. The nets are
computed once and fed to the net-based checks.

The verify command is now a thin renderer over the report, byte-identical
output. Prune the now-dead nets.hpp / bsdl_check.hpp / <unordered_set>
includes from commands.cpp.

Add tests/test_verify.cpp: builds small systems by hand and asserts the
report (empty system, Power/GndShield bridged-net inconsistency, orphan
counts by import origin) — pure core, no UI.

This is the structuring extraction: the same VerifyReport can now back the
analyze screen's Issues pane and the dashboard health rows, removing the
triple duplication of passes 1-3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 19:51:53 +02:00
parent cccc5f131d
commit e3350b8d95
4 changed files with 272 additions and 95 deletions

80
tests/test_verify.cpp Normal file
View File

@@ -0,0 +1,80 @@
#include <doctest/doctest.h>
#include "core/app/verify.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"
// app::verify is pure core: it takes a System* and returns a VerifyReport of
// structured findings, with no Print/dialog/FTXUI. These tests build small
// systems by hand and assert the report — no UI involved.
TEST_CASE("verify on a null or empty system reports nothing") {
app::VerifyReport none = app::verify(nullptr);
CHECK(none.typed_pins == 0);
CHECK(none.total_nets == 0);
CHECK(none.role_mismatches.empty());
System sys;
app::VerifyReport r = app::verify(&sys);
CHECK(r.typed_pins == 0);
CHECK(r.total_nets == 0);
CHECK(r.bridged_nets == 0);
CHECK(r.net_inconsistencies.empty());
CHECK(r.orphan_total() == 0);
CHECK(r.model_total() == 0);
}
TEST_CASE("verify flags a bridged net that mixes Power and GndShield") {
// Two cards, one wired pin pair: A.NETA (Power) <-> B.NETB (GndShield).
System sys;
Module *a = sys.modules()->merge("A");
Module *b = sys.modules()->merge("B");
Part *ja = new Part("J1"); a->add(ja);
Part *jb = new Part("P1"); b->add(jb);
Pin *pa = new Pin("1"); ja->add(pa);
Pin *pb = new Pin("1"); jb->add(pb);
Signal *sa = a->signals->merge("NETA"); sa->type = SignalType::Power;
Signal *sb = b->signals->merge("NETB"); sb->type = SignalType::GndShield;
sa->add(pa); pa->connect(sa);
sb->add(pb); pb->connect(sb);
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
c->transform_name = "identity";
c->pin_map.emplace_back(pa, pb);
sys.connections()->add(c);
app::VerifyReport r = app::verify(&sys);
CHECK(r.total_nets == 1);
CHECK(r.bridged_nets == 1);
REQUIRE(r.net_inconsistencies.size() == 1);
CHECK(r.net_inconsistencies[0].members.size() == 2);
// Both endpoints are present with their declared types.
bool seen_power = false, seen_gnd = false;
for (const auto &m : r.net_inconsistencies[0].members) {
if (m.type == SignalType::Power) seen_power = true;
if (m.type == SignalType::GndShield) seen_gnd = true;
}
CHECK(seen_power);
CHECK(seen_gnd);
}
TEST_CASE("verify counts orphan pins by their import origin") {
System sys;
Module *m = sys.modules()->merge("M");
Part *p = new Part("J1"); m->add(p);
Pin *imp = new Pin("1"); imp->nc_origin = NcOrigin::ImportedUnconnected; p->add(imp);
Pin *drp = new Pin("2"); drp->nc_origin = NcOrigin::DroppedSingleton; p->add(drp);
Pin *wired = new Pin("3"); p->add(wired);
Signal *s = m->signals->merge("NET"); s->add(wired); wired->connect(s);
app::VerifyReport r = app::verify(&sys);
CHECK(r.orphan_imported == 1);
CHECK(r.orphan_dropped == 1);
CHECK(r.orphan_total() == 2);
}