Altium import, nets, canonical pins, component kinds, set/$var, scrollback, source modal.
Major additions, all wired end-to-end with doctest coverage:
- Altium netlist importer (`imports/import_altium.{hpp,cpp}`): two-pass
parser for `[ ]` parts and `( )` signals; `System::Load` no longer has
the IMPORT_ALTIUM hole.
- `duplicate <src> <dst>` deep-copies a module (signals, parts, pins,
rewired signals); connections excluded by design.
- Nets (`system/nets.{hpp,cpp}`): BFS over `Connection::pin_map` to
return the transitive (Module, Signal) closure. `verify` extended with
a second pass flagging Power↔GndShield inconsistencies in bridged
nets; new `net <module> <signal>` command for inspection.
- Canonical pin names (`system/pin_name.{hpp,cpp}`): zero-padded digit
suffix lets A1 ↔ A001 pair via `IdentityTransform` and
`CheckIdentityCompatible` without losing the imported notation.
- Component classification (`system/component_kind.{hpp,cpp}`):
`Part::kind` inferred at construction from the reference-designator
prefix (longest-match: LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV first,
then R/C/L/F/D/Q/U/J/P/Y/X/S).
- Identity wiring tolerance: `CheckIdentityCompatible` accepts the
subset case (typical when one importer drops NC pins, e.g. Altium)
and surfaces orphans as an info string. `FillIdentityNCs`
materialises orphan canonical positions as NC pins on the missing
side at connect time.
- Connector layout preparation: `pin_layout(kind)` and
`FillPartFromLayout(part, kind)` stubs in `pin_role`, called from
`set-type`. Empty today; populate alongside `vpx_3u_role`.
- TUI scrollback: PageUp/PageDown step 10 lines, Home/End jump to
ends; `Print()` snaps back to the tail.
- `set <name> <value>` declares session variables; `$name` / `${name}`
expanded inside `Finalize` between canonical-form recording and the
action call — history and script-save preserve `$var` references.
- Long `source` scripts now show a centred "Computing…" modal with a
N/M progress counter. Driven by a ticker thread that posts one
paced `Event::Special` per processed line, ack'd by the main thread,
so heavy lines don't backlog ticks and freeze the counter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
70
tests/test_component_kind.cpp
Normal file
70
tests/test_component_kind.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/component_kind.hpp"
|
||||
#include "system/parts.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
TEST_CASE("infer_component_kind: passives") {
|
||||
CHECK(infer_component_kind("R12") == ComponentKind::Passive);
|
||||
CHECK(infer_component_kind("C1") == ComponentKind::Passive);
|
||||
CHECK(infer_component_kind("L4") == ComponentKind::Passive);
|
||||
CHECK(infer_component_kind("FB1") == ComponentKind::Passive);
|
||||
CHECK(infer_component_kind("RN2") == ComponentKind::Passive);
|
||||
CHECK(infer_component_kind("F3") == ComponentKind::Passive); // fuse
|
||||
}
|
||||
|
||||
TEST_CASE("infer_component_kind: semiconductors") {
|
||||
CHECK(infer_component_kind("D1") == ComponentKind::Semiconductor);
|
||||
CHECK(infer_component_kind("Q4") == ComponentKind::Semiconductor);
|
||||
CHECK(infer_component_kind("LED2") == ComponentKind::Semiconductor);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_component_kind: ICs") {
|
||||
CHECK(infer_component_kind("U1") == ComponentKind::IntegratedCircuit);
|
||||
CHECK(infer_component_kind("U100") == ComponentKind::IntegratedCircuit);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_component_kind: connectors") {
|
||||
CHECK(infer_component_kind("J1") == ComponentKind::Connector);
|
||||
CHECK(infer_component_kind("P0") == ComponentKind::Connector);
|
||||
CHECK(infer_component_kind("J100") == ComponentKind::Connector);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_component_kind: misc") {
|
||||
CHECK(infer_component_kind("TP1") == ComponentKind::TestPoint);
|
||||
CHECK(infer_component_kind("SW1") == ComponentKind::Switch);
|
||||
CHECK(infer_component_kind("Y1") == ComponentKind::Crystal);
|
||||
CHECK(infer_component_kind("X1") == ComponentKind::Crystal);
|
||||
CHECK(infer_component_kind("MK1") == ComponentKind::Mechanical);
|
||||
CHECK(infer_component_kind("HS2") == ComponentKind::Mechanical);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_component_kind: fallback") {
|
||||
CHECK(infer_component_kind("") == ComponentKind::Other);
|
||||
CHECK(infer_component_kind("123") == ComponentKind::Other); // no letter prefix
|
||||
CHECK(infer_component_kind("ZZZ1") == ComponentKind::Other); // unknown prefix
|
||||
}
|
||||
|
||||
TEST_CASE("infer_component_kind: case insensitive") {
|
||||
CHECK(infer_component_kind("r1") == ComponentKind::Passive);
|
||||
CHECK(infer_component_kind("u1") == ComponentKind::IntegratedCircuit);
|
||||
}
|
||||
|
||||
TEST_CASE("Part ctor populates kind from name") {
|
||||
auto r = std::make_unique<Part>("R12");
|
||||
auto u = std::make_unique<Part>("U7");
|
||||
auto j = std::make_unique<Part>("J1");
|
||||
CHECK(r->kind == ComponentKind::Passive);
|
||||
CHECK(u->kind == ComponentKind::IntegratedCircuit);
|
||||
CHECK(j->kind == ComponentKind::Connector);
|
||||
}
|
||||
|
||||
TEST_CASE("component_kind_from_name accepts canonical and aliases") {
|
||||
ComponentKind k;
|
||||
CHECK(component_kind_from_name("passive", k)); CHECK(k == ComponentKind::Passive);
|
||||
CHECK(component_kind_from_name("ic", k)); CHECK(k == ComponentKind::IntegratedCircuit);
|
||||
CHECK(component_kind_from_name("conn", k)); CHECK(k == ComponentKind::Connector);
|
||||
CHECK(component_kind_from_name("Connector", k)); CHECK(k == ComponentKind::Connector);
|
||||
CHECK(!component_kind_from_name("nonsense", k));
|
||||
}
|
||||
102
tests/test_pin_name.cpp
Normal file
102
tests/test_pin_name.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_name.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/transform.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
TEST_CASE("canonical_pin_name: zero-pads pure digit suffix to 3") {
|
||||
CHECK(canonical_pin_name("A1") == "A001");
|
||||
CHECK(canonical_pin_name("A001") == "A001");
|
||||
CHECK(canonical_pin_name("AB12") == "AB012");
|
||||
CHECK(canonical_pin_name("12") == "012");
|
||||
CHECK(canonical_pin_name("J1") == "J001");
|
||||
CHECK(canonical_pin_name("J999") == "J999");
|
||||
CHECK(canonical_pin_name("J1000") == "J1000");
|
||||
}
|
||||
|
||||
TEST_CASE("canonical_pin_name: leaves names without digit suffix unchanged") {
|
||||
CHECK(canonical_pin_name("") == "");
|
||||
CHECK(canonical_pin_name("VCC") == "VCC");
|
||||
CHECK(canonical_pin_name("GND") == "GND");
|
||||
}
|
||||
|
||||
TEST_CASE("canonical_pin_name: leaves mixed suffixes unchanged") {
|
||||
CHECK(canonical_pin_name("A1B") == "A1B");
|
||||
CHECK(canonical_pin_name("P3.3V") == "P3.3V");
|
||||
CHECK(canonical_pin_name("D+") == "D+");
|
||||
}
|
||||
|
||||
TEST_CASE("IdentityTransform pairs A1 ↔ A001 and reports compatible") {
|
||||
auto a = std::make_unique<Part>("U_a");
|
||||
auto b = std::make_unique<Part>("U_b");
|
||||
Pin *a1 = new Pin("A1");
|
||||
Pin *a2 = new Pin("A2");
|
||||
Pin *b1 = new Pin("A001");
|
||||
Pin *b2 = new Pin("A002");
|
||||
a->add(a1); a->add(a2);
|
||||
b->add(b1); b->add(b2);
|
||||
|
||||
CHECK(CheckIdentityCompatible(a.get(), b.get()) == "");
|
||||
|
||||
IdentityTransform t;
|
||||
auto wires = t.apply(a.get(), b.get());
|
||||
CHECK(wires.size() == 2);
|
||||
// Build a quick set for order-independent verification.
|
||||
bool saw_a1 = false, saw_a2 = false;
|
||||
for (auto &w : wires) {
|
||||
if (w.first == a1) { CHECK(w.second == b1); saw_a1 = true; }
|
||||
if (w.first == a2) { CHECK(w.second == b2); saw_a2 = true; }
|
||||
}
|
||||
CHECK(saw_a1);
|
||||
CHECK(saw_a2);
|
||||
}
|
||||
|
||||
TEST_CASE("CheckIdentityCompatible accepts subset, surfaces orphans as info") {
|
||||
auto a = std::make_unique<Part>("U_a");
|
||||
auto b = std::make_unique<Part>("U_b");
|
||||
a->add(new Pin("A1"));
|
||||
a->add(new Pin("A2"));
|
||||
b->add(new Pin("A001")); // canonical match for A1
|
||||
// b is a strict subset of a — accept, info mentions A2 (orphan on a).
|
||||
std::string info;
|
||||
std::string err = CheckIdentityCompatible(a.get(), b.get(), &info);
|
||||
CHECK(err.empty());
|
||||
CHECK(!info.empty());
|
||||
CHECK(info.find("A2") != std::string::npos);
|
||||
CHECK(info.find("U_a") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("FillIdentityNCs materialises the missing side") {
|
||||
auto a = std::make_unique<Part>("U_a");
|
||||
auto b = std::make_unique<Part>("U_b");
|
||||
a->add(new Pin("A1"));
|
||||
a->add(new Pin("A2"));
|
||||
a->add(new Pin("A3"));
|
||||
b->add(new Pin("A001"));
|
||||
// b is a strict subset; A2/A3 missing → 2 created on b.
|
||||
int added = FillIdentityNCs(a.get(), b.get());
|
||||
CHECK(added == 2);
|
||||
CHECK(b->size() == 3);
|
||||
CHECK(b->exists("A2"));
|
||||
CHECK(b->exists("A3"));
|
||||
// Idempotent.
|
||||
CHECK(FillIdentityNCs(a.get(), b.get()) == 0);
|
||||
// Materialised pins are NC.
|
||||
CHECK(b->get("A2")->signal() == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("CheckIdentityCompatible refuses bidirectional mismatch") {
|
||||
auto a = std::make_unique<Part>("U_a");
|
||||
auto b = std::make_unique<Part>("U_b");
|
||||
a->add(new Pin("A1"));
|
||||
a->add(new Pin("X9")); // only on a
|
||||
b->add(new Pin("A001")); // canonical match
|
||||
b->add(new Pin("Y7")); // only on b
|
||||
std::string err = CheckIdentityCompatible(a.get(), b.get());
|
||||
CHECK(!err.empty());
|
||||
CHECK(err.find("X9") != std::string::npos);
|
||||
CHECK(err.find("Y7") != std::string::npos);
|
||||
}
|
||||
Reference in New Issue
Block a user