From b9994461513f82fcf6b780eac332459606aebaae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 21:18:46 +0200 Subject: [PATCH] Extract attach-bsdl into core; add it to the wx GUI. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second editing op into the wx frontend. Extract the logic (parse the .bsd, apply it to the part, record bsdl_path) into core/app/edit.hpp::attach_bsdl (Part*, path) -> {ok, error, entity, bound, unbound, ports_total}, failing without mutation when the file can't be parsed. The TUI `attach-bsdl` command now renders that result (output unchanged); the wx GUI gains an Edit ▸ Attach BSDL… menu reusing PickPart() + a .bsd file dialog. Prune the now-dead bsdl_model include from commands.cpp. tests/test_edit.cpp: parse failure leaves the part untouched; null part. The success path is covered by a batch run (entity + bound/total ports). 381 core assertions green; tui + wx build clean. Co-Authored-By: Claude Opus 4.8 --- src/core/app/edit.cpp | 26 ++++++++++++++++++++++++++ src/core/app/edit.hpp | 16 ++++++++++++++++ src/frontends/tui/commands.cpp | 15 ++++----------- src/frontends/wx/wx_frame.cpp | 27 +++++++++++++++++++++++++++ src/frontends/wx/wx_frame.hpp | 1 + tests/test_edit.cpp | 17 +++++++++++++++++ 6 files changed, 91 insertions(+), 11 deletions(-) diff --git a/src/core/app/edit.cpp b/src/core/app/edit.cpp index d039d2d..65f4c2b 100644 --- a/src/core/app/edit.cpp +++ b/src/core/app/edit.cpp @@ -1,5 +1,6 @@ #include "core/app/edit.hpp" +#include "core/domain/bsdl_model.hpp" #include "core/domain/parts.hpp" #include "core/domain/pin_model.hpp" #include "core/domain/transform_vpx.hpp" // ValidatePartForKind @@ -28,4 +29,29 @@ SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind) return r; } +AttachBsdlResult attach_bsdl(Part *part, const std::string &path) +{ + AttachBsdlResult r; + if (!part) { + r.error = "no part"; + return r; + } + + BsdlModel model = BsdlModel::from_file(path); + if (!model.valid()) { + r.error = "cannot parse " + path + + (model.error().empty() ? "" : (": " + model.error())); + return r; + } + + BsdlApplyReport rep = apply_bsdl(part, model); + part->bsdl_path = path; + r.entity = model.entity(); + r.bound = rep.bound; + r.unbound = rep.unbound; + r.ports_total = (int)model.ports().size(); + r.ok = true; + return r; +} + } // namespace app diff --git a/src/core/app/edit.hpp b/src/core/app/edit.hpp index bfe43f7..9dc52f8 100644 --- a/src/core/app/edit.hpp +++ b/src/core/app/edit.hpp @@ -22,6 +22,22 @@ struct SetConnectorTypeResult { // (ok=false, error set, no mutation) when the kind is invalid for the part. SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind); +// Outcome of attaching a BSDL model to a part. On success the part's pin specs +// are filled from the model and its bsdl_path is recorded. +struct AttachBsdlResult { + bool ok = false; + std::string error; ///< set when the .bsd cannot be parsed + std::string entity; ///< the BSDL entity name + int bound = 0; ///< ports matched to a pin + int unbound = 0; ///< ports with no matching pin + int ports_total = 0; ///< ports declared in the model +}; + +// Parse the BSDL file at `path` and apply it to `part` (fills each pin's role +// and direction; records bsdl_path). Fails (ok=false, error set, no mutation) +// when the file cannot be parsed. +AttachBsdlResult attach_bsdl(Part *part, const std::string &path); + } // namespace app #endif // _APP_EDIT_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index 761a9f0..31e4537 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -7,7 +7,6 @@ #include "core/domain/parts.hpp" #include "core/domain/persist.hpp" #include "core/domain/pin_role.hpp" -#include "core/domain/bsdl_model.hpp" #include "core/domain/pins.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" @@ -445,17 +444,11 @@ void Tui::RegisterCommands() { } } - BsdlModel model = BsdlModel::from_file(args[2]); - if (!model.valid()) { - Print("attach-bsdl: cannot parse " + args[2] - + (model.error().empty() ? "" : (": " + model.error()))); - return; - } - BsdlApplyReport r = apply_bsdl(prt, model); - prt->bsdl_path = args[2]; - Print(mod->name + "/" + prt->name + ": attached BSDL '" + model.entity() + app::AttachBsdlResult r = app::attach_bsdl(prt, args[2]); + if (!r.ok) { Print("attach-bsdl: " + r.error); return; } + Print(mod->name + "/" + prt->name + ": attached BSDL '" + r.entity + "' — " + std::to_string(r.bound) + "/" - + std::to_string((int)model.ports().size()) + " ports bound" + + std::to_string(r.ports_total) + " ports bound" + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : "")); }, /*prompt_for_missing=*/ false, diff --git a/src/frontends/wx/wx_frame.cpp b/src/frontends/wx/wx_frame.cpp index 0b08e4b..2f89124 100644 --- a/src/frontends/wx/wx_frame.cpp +++ b/src/frontends/wx/wx_frame.cpp @@ -33,6 +33,7 @@ enum { ID_SAVE, ID_EXPORT, ID_SET_CONNECTOR_TYPE, + ID_ATTACH_BSDL, ID_VERIFY, ID_QUIT, ID_ABOUT, @@ -57,6 +58,7 @@ EssimFrame::EssimFrame(WxFrontend &fe) auto *edit = new wxMenu; edit->Append(ID_SET_CONNECTOR_TYPE, "Set &connector type…\tCtrl-T"); + edit->Append(ID_ATTACH_BSDL, "Attach &BSDL…\tCtrl-B"); auto *sysm = new wxMenu; sysm->Append(ID_VERIFY, "&Verify\tCtrl-K"); @@ -100,6 +102,7 @@ EssimFrame::EssimFrame(WxFrontend &fe) Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE); Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT); Bind(wxEVT_MENU, &EssimFrame::OnSetConnectorType, this, ID_SET_CONNECTOR_TYPE); + Bind(wxEVT_MENU, &EssimFrame::OnAttachBsdl, this, ID_ATTACH_BSDL); Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY); Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT); Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT); @@ -309,6 +312,30 @@ void EssimFrame::OnSetConnectorType(wxCommandEvent &) { RebuildModelView(); } +void EssimFrame::OnAttachBsdl(wxCommandEvent &) { + Part *p = PickPart(); + if (!p) return; + + wxFileDialog dlg(this, "Attach a BSDL model", "", "", + "BSDL files (*.bsd)|*.bsd|All files (*.*)|*.*", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dlg.ShowModal() != wxID_OK) return; + + app::AttachBsdlResult r = app::attach_bsdl(p, dlg.GetPath().utf8_string()); + if (!r.ok) { + Log("attach-bsdl: " + wx(r.error)); + wxMessageBox(wx(r.error), "Attach BSDL failed", wxOK | wxICON_ERROR, this); + return; + } + + wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name); + wxString tail = r.unbound ? wxString::Format(", %d unbound", r.unbound) + : wxString(); + Log(wxString::Format("%s: attached BSDL '%s' — %d/%d ports bound%s", + who, wx(r.entity), r.bound, r.ports_total, tail)); + RebuildModelView(); +} + void EssimFrame::OnVerify(wxCommandEvent &) { app::VerifyReport r = app::verify(fe_.system()); diff --git a/src/frontends/wx/wx_frame.hpp b/src/frontends/wx/wx_frame.hpp index 6121b4a..ff82a61 100644 --- a/src/frontends/wx/wx_frame.hpp +++ b/src/frontends/wx/wx_frame.hpp @@ -23,6 +23,7 @@ private: void OnSave(wxCommandEvent &); void OnExport(wxCommandEvent &); void OnSetConnectorType(wxCommandEvent &); + void OnAttachBsdl(wxCommandEvent &); void OnVerify(wxCommandEvent &); void OnQuit(wxCommandEvent &); void OnAbout(wxCommandEvent &); diff --git a/tests/test_edit.cpp b/tests/test_edit.cpp index a4e891c..8007436 100644 --- a/tests/test_edit.cpp +++ b/tests/test_edit.cpp @@ -37,3 +37,20 @@ TEST_CASE("set_connector_type on a null part fails cleanly") { CHECK_FALSE(r.ok); CHECK_FALSE(r.error.empty()); } + +TEST_CASE("attach_bsdl reports a parse failure without mutating the part") { + Part p("J1"); + p.add(new Pin("1")); + + app::AttachBsdlResult r = app::attach_bsdl(&p, "/nonexistent-xyz/none.bsd"); + + CHECK_FALSE(r.ok); + CHECK(r.error.find("cannot parse") != std::string::npos); + CHECK(p.bsdl_path.empty()); // failure leaves the part untouched +} + +TEST_CASE("attach_bsdl on a null part fails cleanly") { + app::AttachBsdlResult r = app::attach_bsdl(nullptr, "x.bsd"); + CHECK_FALSE(r.ok); + CHECK_FALSE(r.error.empty()); +}