From b36af3167a5cc9f5185ddfe427b9832dd29558da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 20:18:35 +0200 Subject: [PATCH] Extract load into core (app::load_module); thin the command. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the import orchestration — System::Load + drop_singleton_signals + infer_signal_types + the post-import counts — out of the `load` command into core/app/load.{hpp,cpp}: app::load_module(System*, name, path, ImportType) returns a LoadResult (ok/error, parts, signals, dropped, power/gnd/kept_other) with no Print/dialog/FTXUI. The "mentor|altium|ods" string→enum mapping moves to app::import_type_from_name (mirrors export_format_from_path). The command only parses the type and renders the counts; output is byte-identical. Add tests/test_load.cpp (core, no UI): the name mapping; a minimal Mentor netlist that imports two parts and drops one singleton signal; and a pin test of the pre-existing missing-file behaviour (ImportBase doesn't check is_open(), so a missing file yields an empty module rather than an error — preserved by the extraction and pinned so any future hardening is a deliberate change). Co-Authored-By: Claude Opus 4.8 --- src/core/app/load.cpp | 46 ++++++++++++++++++++++++ src/core/app/load.hpp | 36 +++++++++++++++++++ src/frontends/tui/commands.cpp | 37 +++++++++----------- tests/test_load.cpp | 64 ++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 src/core/app/load.cpp create mode 100644 src/core/app/load.hpp create mode 100644 tests/test_load.cpp diff --git a/src/core/app/load.cpp b/src/core/app/load.cpp new file mode 100644 index 0000000..3a4f94e --- /dev/null +++ b/src/core/app/load.cpp @@ -0,0 +1,46 @@ +#include "core/app/load.hpp" + +#include "core/domain/analysis.hpp" // infer_signal_types, SignalTypeInferenceStats +#include "core/domain/modules.hpp" +#include "core/domain/signals.hpp" // drop_singleton_signals +#include "core/domain/system.hpp" + +#include +#include + +namespace app { + +bool import_type_from_name(const std::string &name, ImportType &out) +{ + std::string ls; + ls.reserve(name.size()); + for (char c : name) ls += (char)std::tolower((unsigned char)c); + if (ls == "mentor") { out = ImportType::IMPORT_MENTOR; return true; } + if (ls == "altium") { out = ImportType::IMPORT_ALTIUM; return true; } + if (ls == "ods") { out = ImportType::IMPORT_ODS; return true; } + return false; +} + +LoadResult load_module(System *sys, const std::string &module_name, + const std::string &path, ImportType type) +{ + LoadResult r; + if (!sys) { r.error = "no system"; return r; } + try { + sys->Load(module_name, path, type); + Module *mod = sys->modules()->get(module_name); + r.dropped = drop_singleton_signals(mod->signals); + SignalTypeInferenceStats inf = infer_signal_types(sys); + r.parts = (int)mod->size(); + r.signals = (int)mod->signals->size(); + r.power = inf.power; + r.gnd = inf.gnd; + r.kept_other = inf.kept_other; + r.ok = true; + } catch (const std::exception &e) { + r.error = e.what(); + } + return r; +} + +} // namespace app diff --git a/src/core/app/load.hpp b/src/core/app/load.hpp new file mode 100644 index 0000000..93308c3 --- /dev/null +++ b/src/core/app/load.hpp @@ -0,0 +1,36 @@ +#ifndef _APP_LOAD_HPP_ +#define _APP_LOAD_HPP_ + +#include "core/domain/system.hpp" // ImportType + +#include + +// Application layer: UI-independent operations that any frontend (TUI, GUI, …) +// can call. No console, no dialogs, no FTXUI — just System in, result out. +namespace app { + +// Map an import-type name (mentor / altium / ods, case-insensitive) to an +// ImportType. Returns false if the name is none of those. +bool import_type_from_name(const std::string &name, ImportType &out); + +// Outcome of loading a module: the post-import counts the caller renders. +struct LoadResult { + bool ok = false; + std::string error; ///< human-readable, set when !ok + int parts = 0; + int signals = 0; + int dropped = 0; ///< singleton/NC signals removed after import + int power = 0; ///< signals inferred Power (name + structure) + int gnd = 0; ///< signals inferred GndShield (name) + int kept_other = 0; ///< name said Power but evidence too weak → kept Other +}; + +// Import a module from a netlist/pinout file into `sys`, drop singleton signals, +// then infer signal types. Returns the counts or an error. Pure core — safe to +// call from any frontend. +LoadResult load_module(System *sys, const std::string &module_name, + const std::string &path, ImportType type); + +} // namespace app + +#endif // _APP_LOAD_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index d80c7c7..4dcd472 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -14,6 +14,7 @@ #include "core/domain/system.hpp" #include "core/app/connect.hpp" +#include "core/app/load.hpp" #include "core/app/verify.hpp" #include "core/domain/transform_vpx.hpp" // ValidatePartForKind @@ -137,29 +138,23 @@ void Tui::RegisterCommands() { {"import type [mentor|altium|ods]", Completion::None}}, [this](const std::vector &args) { if (!sys) { Print("no system: run 'new' first."); return; } - std::string ls = ToLower(args[2]); ImportType t; - if (ls == "mentor") t = ImportType::IMPORT_MENTOR; - else if (ls == "altium") t = ImportType::IMPORT_ALTIUM; - else if (ls == "ods") t = ImportType::IMPORT_ODS; - else { Print("unknown import type: " + args[2]); return; } - try { - sys->Load(args[0], args[1], t); - Module *mod = sys->modules()->get(args[0]); - int dropped = drop_singleton_signals(mod->signals); - auto inf = infer_signal_types(sys.get()); - Print("loaded '" + args[0] + "' from " + args[1]); - Print(" parts: " + std::to_string(mod->size())); - Print(" signals: " + std::to_string(mod->signals->size()) - + (dropped ? " (dropped " + std::to_string(dropped) - + " singleton/NC signal(s))" : "")); - Print(" types: " + std::to_string(inf.power) + " power, " - + std::to_string(inf.gnd) + " gnd, " - + std::to_string(inf.kept_other) - + " suspect Power (name only — kept as Other)"); - } catch (const std::exception &e) { - Print(std::string("load failed: ") + e.what()); + if (!app::import_type_from_name(args[2], t)) { + Print("unknown import type: " + args[2]); return; } + // Import + drop-singletons + infer-types is one core op; the command + // only parses the type and renders the counts. + app::LoadResult r = app::load_module(sys.get(), args[0], args[1], t); + if (!r.ok) { Print(std::string("load failed: ") + r.error); return; } + Print("loaded '" + args[0] + "' from " + args[1]); + Print(" parts: " + std::to_string(r.parts)); + Print(" signals: " + std::to_string(r.signals) + + (r.dropped ? " (dropped " + std::to_string(r.dropped) + + " singleton/NC signal(s))" : "")); + Print(" types: " + std::to_string(r.power) + " power, " + + std::to_string(r.gnd) + " gnd, " + + std::to_string(r.kept_other) + + " suspect Power (name only — kept as Other)"); }, /*prompt_for_missing=*/ true, "load a module from a netlist / pinout file (mentor, altium, ods)", diff --git a/tests/test_load.cpp b/tests/test_load.cpp new file mode 100644 index 0000000..db70413 --- /dev/null +++ b/tests/test_load.cpp @@ -0,0 +1,64 @@ +#include + +#include "core/app/load.hpp" +#include "core/domain/system.hpp" + +#include +#include +#include + +// app::load_module is pure core: import a module, drop singleton signals, infer +// signal types, return counts or an error — no Print/dialog/FTXUI. The parse +// helper import_type_from_name is likewise UI-free. + +TEST_CASE("import_type_from_name maps names case-insensitively") { + ImportType t; + CHECK(app::import_type_from_name("mentor", t)); + CHECK(t == ImportType::IMPORT_MENTOR); + CHECK(app::import_type_from_name("ALTIUM", t)); + CHECK(t == ImportType::IMPORT_ALTIUM); + CHECK(app::import_type_from_name("Ods", t)); + CHECK(t == ImportType::IMPORT_ODS); + CHECK_FALSE(app::import_type_from_name("kicad", t)); + CHECK_FALSE(app::import_type_from_name("", t)); +} + +TEST_CASE("load_module imports, drops singletons and reports counts") { + // Minimal Mentor netlist: two parts; NETA/NETB span both parts (2 pins + // each, kept), LONELY sits on one pin only (dropped as a singleton). + const char *path = "test_load_in.net"; + { + std::ofstream f(path); + f << "COMP: 'C1' 'J1'\n" + " Explicit Pin: '1' 'x' 'NETA'\n" + " Explicit Pin: '2' 'x' 'NETB'\n" + " Explicit Pin: '3' 'x' 'LONELY'\n" + "COMP: 'C2' 'J2'\n" + " Explicit Pin: '1' 'x' 'NETA'\n" + " Explicit Pin: '2' 'x' 'NETB'\n"; + } + + System sys; + app::LoadResult r = app::load_module(&sys, "M", path, ImportType::IMPORT_MENTOR); + + CHECK(r.ok); + CHECK(r.error.empty()); + CHECK(r.parts == 2); + CHECK(r.signals == 2); // NETA, NETB — LONELY dropped + CHECK(r.dropped == 1); // LONELY + + std::remove(path); +} + +TEST_CASE("load_module on a missing file currently succeeds empty (no throw)") { + // Pre-existing behaviour, preserved by the extraction: ImportBase opens the + // stream without checking is_open(), so a missing file yields an empty + // module rather than an error. Pinned here so any future hardening of this + // path is a deliberate, visible change. + System sys; + app::LoadResult r = app::load_module( + &sys, "M", "/nonexistent-dir-xyz/nope.net", ImportType::IMPORT_MENTOR); + CHECK(r.ok); + CHECK(r.parts == 0); + CHECK(r.signals == 0); +}