Extract attach-bsdl into core; add it to the wx GUI.

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:18:46 +02:00
parent 7e88f82446
commit b999446151
6 changed files with 91 additions and 11 deletions

View File

@@ -1,5 +1,6 @@
#include "core/app/edit.hpp" #include "core/app/edit.hpp"
#include "core/domain/bsdl_model.hpp"
#include "core/domain/parts.hpp" #include "core/domain/parts.hpp"
#include "core/domain/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind #include "core/domain/transform_vpx.hpp" // ValidatePartForKind
@@ -28,4 +29,29 @@ SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind)
return r; 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 } // namespace app

View File

@@ -22,6 +22,22 @@ struct SetConnectorTypeResult {
// (ok=false, error set, no mutation) when the kind is invalid for the part. // (ok=false, error set, no mutation) when the kind is invalid for the part.
SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind); 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 } // namespace app
#endif // _APP_EDIT_HPP_ #endif // _APP_EDIT_HPP_

View File

@@ -7,7 +7,6 @@
#include "core/domain/parts.hpp" #include "core/domain/parts.hpp"
#include "core/domain/persist.hpp" #include "core/domain/persist.hpp"
#include "core/domain/pin_role.hpp" #include "core/domain/pin_role.hpp"
#include "core/domain/bsdl_model.hpp"
#include "core/domain/pins.hpp" #include "core/domain/pins.hpp"
#include "core/domain/signals.hpp" #include "core/domain/signals.hpp"
#include "core/domain/system.hpp" #include "core/domain/system.hpp"
@@ -445,17 +444,11 @@ void Tui::RegisterCommands() {
} }
} }
BsdlModel model = BsdlModel::from_file(args[2]); app::AttachBsdlResult r = app::attach_bsdl(prt, args[2]);
if (!model.valid()) { if (!r.ok) { Print("attach-bsdl: " + r.error); return; }
Print("attach-bsdl: cannot parse " + args[2] Print(mod->name + "/" + prt->name + ": attached BSDL '" + r.entity
+ (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()
+ "' — " + std::to_string(r.bound) + "/" + "' — " + 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") : "")); + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
}, },
/*prompt_for_missing=*/ false, /*prompt_for_missing=*/ false,

View File

@@ -33,6 +33,7 @@ enum {
ID_SAVE, ID_SAVE,
ID_EXPORT, ID_EXPORT,
ID_SET_CONNECTOR_TYPE, ID_SET_CONNECTOR_TYPE,
ID_ATTACH_BSDL,
ID_VERIFY, ID_VERIFY,
ID_QUIT, ID_QUIT,
ID_ABOUT, ID_ABOUT,
@@ -57,6 +58,7 @@ EssimFrame::EssimFrame(WxFrontend &fe)
auto *edit = new wxMenu; auto *edit = new wxMenu;
edit->Append(ID_SET_CONNECTOR_TYPE, "Set &connector type…\tCtrl-T"); edit->Append(ID_SET_CONNECTOR_TYPE, "Set &connector type…\tCtrl-T");
edit->Append(ID_ATTACH_BSDL, "Attach &BSDL…\tCtrl-B");
auto *sysm = new wxMenu; auto *sysm = new wxMenu;
sysm->Append(ID_VERIFY, "&Verify\tCtrl-K"); 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::OnSave, this, ID_SAVE);
Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT); Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT);
Bind(wxEVT_MENU, &EssimFrame::OnSetConnectorType, this, ID_SET_CONNECTOR_TYPE); 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::OnVerify, this, ID_VERIFY);
Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT); Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT);
Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT); Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT);
@@ -309,6 +312,30 @@ void EssimFrame::OnSetConnectorType(wxCommandEvent &) {
RebuildModelView(); 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 &) { void EssimFrame::OnVerify(wxCommandEvent &) {
app::VerifyReport r = app::verify(fe_.system()); app::VerifyReport r = app::verify(fe_.system());

View File

@@ -23,6 +23,7 @@ private:
void OnSave(wxCommandEvent &); void OnSave(wxCommandEvent &);
void OnExport(wxCommandEvent &); void OnExport(wxCommandEvent &);
void OnSetConnectorType(wxCommandEvent &); void OnSetConnectorType(wxCommandEvent &);
void OnAttachBsdl(wxCommandEvent &);
void OnVerify(wxCommandEvent &); void OnVerify(wxCommandEvent &);
void OnQuit(wxCommandEvent &); void OnQuit(wxCommandEvent &);
void OnAbout(wxCommandEvent &); void OnAbout(wxCommandEvent &);

View File

@@ -37,3 +37,20 @@ TEST_CASE("set_connector_type on a null part fails cleanly") {
CHECK_FALSE(r.ok); CHECK_FALSE(r.ok);
CHECK_FALSE(r.error.empty()); 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());
}