Extract load into core (app::load_module); thin the command.
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 <noreply@anthropic.com>
This commit is contained in:
46
src/core/app/load.cpp
Normal file
46
src/core/app/load.cpp
Normal file
@@ -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 <cctype>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
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
|
||||||
36
src/core/app/load.hpp
Normal file
36
src/core/app/load.hpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#ifndef _APP_LOAD_HPP_
|
||||||
|
#define _APP_LOAD_HPP_
|
||||||
|
|
||||||
|
#include "core/domain/system.hpp" // ImportType
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// 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_
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "core/domain/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include "core/app/connect.hpp"
|
#include "core/app/connect.hpp"
|
||||||
|
#include "core/app/load.hpp"
|
||||||
#include "core/app/verify.hpp"
|
#include "core/app/verify.hpp"
|
||||||
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
|
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
|
||||||
|
|
||||||
@@ -137,29 +138,23 @@ void Tui::RegisterCommands() {
|
|||||||
{"import type [mentor|altium|ods]", Completion::None}},
|
{"import type [mentor|altium|ods]", Completion::None}},
|
||||||
[this](const std::vector<std::string> &args) {
|
[this](const std::vector<std::string> &args) {
|
||||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||||
std::string ls = ToLower(args[2]);
|
|
||||||
ImportType t;
|
ImportType t;
|
||||||
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
|
if (!app::import_type_from_name(args[2], t)) {
|
||||||
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
|
Print("unknown import type: " + args[2]); return;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
// 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,
|
/*prompt_for_missing=*/ true,
|
||||||
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
||||||
|
|||||||
64
tests/test_load.cpp
Normal file
64
tests/test_load.cpp
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "core/app/load.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user