Add a core script engine; wire File ▸ Run script in the wx GUI.
The wx frontend had no way to run essim command scripts (only the tui shell
did). Add a frontend-agnostic engine in core/app/script.{hpp,cpp}:
run_script(unique_ptr<System>&, path, ostream&) -> {ok, error, lines, errors}.
It parses essim scripts (# comments, blank lines, "quoted" args, set + $var /
${var} expansion, nested source with a depth guard) and dispatches the
scriptable, system-building commands — new, load, connect, set-connector-type,
set-signal-type, attach-bsdl, verify, export, save, restore — to the existing
app::* operations, writing their (TUI-identical) output to the stream.
Unsupported/interactive commands are reported and counted, execution continues.
System is taken by reference so new/restore can replace it.
wx gains File ▸ Run script…: pick a .essim, run it, echo the output into the
log and refresh. WxFrontend exposes system_ptr() for the engine.
tests/test_script.cpp: a full script (comment + set/$var + new + load + set-
signal-type + verify) builds the system and produces the expected log; missing
file reported; unsupported command flagged and skipped. 400 core assertions
green; tui + wx build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
320
src/core/app/script.cpp
Normal file
320
src/core/app/script.cpp
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
#include "core/app/script.hpp"
|
||||||
|
|
||||||
|
#include "core/app/connect.hpp"
|
||||||
|
#include "core/app/edit.hpp"
|
||||||
|
#include "core/app/export.hpp"
|
||||||
|
#include "core/app/load.hpp"
|
||||||
|
#include "core/app/verify.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/analysis.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/persist.hpp"
|
||||||
|
#include "core/domain/signal_type.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
|
#include <ostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Whitespace split with "double quotes" grouping — same rules as the TUI shell.
|
||||||
|
std::vector<std::string> tokenize(const std::string &s) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::string cur;
|
||||||
|
bool in_q = false;
|
||||||
|
bool has = false;
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '"') { in_q = !in_q; has = true; continue; }
|
||||||
|
if (!in_q && std::isspace((unsigned char)c)) {
|
||||||
|
if (has) { out.push_back(std::move(cur)); cur.clear(); has = false; }
|
||||||
|
} else {
|
||||||
|
cur.push_back(c);
|
||||||
|
has = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has) out.push_back(std::move(cur));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One script execution: holds the variable table, the System reference (so new/
|
||||||
|
// restore can replace it) and the output stream.
|
||||||
|
class Runner {
|
||||||
|
public:
|
||||||
|
Runner(std::unique_ptr<System> &sys, std::ostream &out) : sys_(sys), out_(out) {}
|
||||||
|
|
||||||
|
// Run a file; `opened` reports whether it could be opened. Returns the count
|
||||||
|
// of effective lines; accumulates command errors into `r.errors`.
|
||||||
|
int run_file(const std::string &path, int depth, ScriptResult &r, bool &opened) {
|
||||||
|
opened = false;
|
||||||
|
if (depth > 32) {
|
||||||
|
emit("source: nesting too deep, skipping " + path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::ifstream f(path);
|
||||||
|
if (!f) return 0;
|
||||||
|
opened = true;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(f, line)) {
|
||||||
|
std::size_t s = line.find_first_not_of(" \t");
|
||||||
|
if (s == std::string::npos) continue;
|
||||||
|
if (line[s] == '#') continue;
|
||||||
|
std::string t = line.substr(s);
|
||||||
|
while (!t.empty() && std::isspace((unsigned char)t.back())) t.pop_back();
|
||||||
|
if (t.empty()) continue;
|
||||||
|
++count;
|
||||||
|
if (!exec(t, depth, r)) ++r.errors;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void emit(const std::string &line) { out_ << line << '\n'; }
|
||||||
|
|
||||||
|
// $name / ${name} → variable value; unknown names kept literally.
|
||||||
|
std::string expand(const std::string &s) const {
|
||||||
|
std::string out;
|
||||||
|
std::size_t i = 0;
|
||||||
|
while (i < s.size()) {
|
||||||
|
if (s[i] != '$') { out.push_back(s[i++]); continue; }
|
||||||
|
std::size_t j = i + 1;
|
||||||
|
bool braces = (j < s.size() && s[j] == '{');
|
||||||
|
if (braces) ++j;
|
||||||
|
std::size_t start = j;
|
||||||
|
while (j < s.size() && (std::isalnum((unsigned char)s[j]) || s[j] == '_')) ++j;
|
||||||
|
std::string name = s.substr(start, j - start);
|
||||||
|
if (braces) {
|
||||||
|
if (j >= s.size() || s[j] != '}') { out.push_back('$'); ++i; continue; }
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (name.empty()) { out.push_back('$'); ++i; continue; }
|
||||||
|
auto it = vars_.find(name);
|
||||||
|
if (it != vars_.end()) out += it->second;
|
||||||
|
else out += s.substr(i, j - i);
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Module *resolve_module(const std::string &name) {
|
||||||
|
try { return sys_->modules()->get(name); }
|
||||||
|
catch (const std::exception &) { emit("unknown module: " + name); return nullptr; }
|
||||||
|
}
|
||||||
|
Part *resolve_part(Module *m, const std::string &name) {
|
||||||
|
try { return m->get(name); }
|
||||||
|
catch (const std::exception &) {
|
||||||
|
emit("part in " + m->name + " not found: " + name);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_verify() {
|
||||||
|
VerifyReport r = verify(sys_.get());
|
||||||
|
for (const auto &m : r.role_mismatches)
|
||||||
|
emit(" " + m.module + "/" + m.part + "/" + m.pin + ": expected "
|
||||||
|
+ signal_type_name(m.expected) + ", got " + signal_type_name(m.actual)
|
||||||
|
+ " (signal: " + m.signal + ")");
|
||||||
|
emit("verify: " + std::to_string(r.role_mismatches.size())
|
||||||
|
+ " local mismatch(es) over " + std::to_string(r.typed_pins)
|
||||||
|
+ " typed pin(s).");
|
||||||
|
for (const auto &ni : r.net_inconsistencies) {
|
||||||
|
std::string line = " net mixes Power and GndShield:";
|
||||||
|
for (const auto &mem : ni.members)
|
||||||
|
line += " " + mem.module + "/" + mem.signal
|
||||||
|
+ "(" + signal_type_name(mem.type) + ")";
|
||||||
|
emit(line);
|
||||||
|
}
|
||||||
|
emit("verify: " + std::to_string(r.net_inconsistencies.size())
|
||||||
|
+ " inconsistent net(s) over " + std::to_string(r.bridged_nets)
|
||||||
|
+ " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
|
||||||
|
emit("verify: " + std::to_string(r.orphan_total())
|
||||||
|
+ " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
|
||||||
|
+ " imported NC, " + std::to_string(r.orphan_dropped)
|
||||||
|
+ " dropped singleton).");
|
||||||
|
auto grp = [&](const std::vector<Anomaly> &v, const char *tail) {
|
||||||
|
for (const auto &a : v)
|
||||||
|
emit(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||||
|
emit("verify: " + std::to_string(v.size()) + tail);
|
||||||
|
};
|
||||||
|
grp(r.pin_anomalies, " model-driven pin anomaly(ies).");
|
||||||
|
grp(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||||
|
grp(r.conflict_anomalies, " source-conflict(s).");
|
||||||
|
grp(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute one already-trimmed line. Returns false on a hard error.
|
||||||
|
bool exec(const std::string &raw, int depth, ScriptResult &top) {
|
||||||
|
std::vector<std::string> tok = tokenize(raw);
|
||||||
|
if (tok.empty()) return true;
|
||||||
|
const std::string cmd = tok[0];
|
||||||
|
std::vector<std::string> a;
|
||||||
|
for (std::size_t i = 1; i < tok.size(); ++i) a.push_back(expand(tok[i]));
|
||||||
|
|
||||||
|
auto need = [&](std::size_t n) {
|
||||||
|
if (a.size() == n) return true;
|
||||||
|
emit(cmd + ": expected " + std::to_string(n) + " argument(s)");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cmd == "set") {
|
||||||
|
if (a.size() != 2) { emit("set: usage: set <name> <value>"); return false; }
|
||||||
|
vars_[a[0]] = a[1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "new") {
|
||||||
|
sys_ = std::make_unique<System>();
|
||||||
|
emit("system created.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "load") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
ImportType t;
|
||||||
|
if (!import_type_from_name(a[2], t)) { emit("unknown import type: " + a[2]); return false; }
|
||||||
|
LoadResult r = load_module(sys_.get(), a[0], a[1], t);
|
||||||
|
if (!r.ok) { emit("load failed: " + r.error); return false; }
|
||||||
|
emit("loaded '" + a[0] + "' from " + a[1]);
|
||||||
|
emit(" parts: " + std::to_string(r.parts));
|
||||||
|
emit(" signals: " + std::to_string(r.signals)
|
||||||
|
+ (r.dropped ? " (dropped " + std::to_string(r.dropped)
|
||||||
|
+ " singleton/NC signal(s))" : ""));
|
||||||
|
emit(" 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)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "connect" || cmd == "plug") {
|
||||||
|
if (!need(4)) return false;
|
||||||
|
Module *m1 = resolve_module(a[0]); if (!m1) return false;
|
||||||
|
Part *p1 = resolve_part(m1, a[1]); if (!p1) return false;
|
||||||
|
Module *m2 = resolve_module(a[2]); if (!m2) return false;
|
||||||
|
Part *p2 = resolve_part(m2, a[3]); if (!p2) return false;
|
||||||
|
ConnectResult r = connect_parts(sys_.get(), m1, p1, m2, p2);
|
||||||
|
if (r.refused) { emit("connect refused: " + r.error); return false; }
|
||||||
|
if (!r.identity_info.empty()) {
|
||||||
|
emit("connect: " + r.identity_info);
|
||||||
|
if (r.nc_added > 0)
|
||||||
|
emit("connect: added " + std::to_string(r.nc_added)
|
||||||
|
+ " NC pin(s) so both sides match");
|
||||||
|
}
|
||||||
|
if (!r.ok) { emit("connect failed: " + r.error); return false; }
|
||||||
|
emit("connected: " + r.connection_name + " via " + r.transform_name
|
||||||
|
+ " (" + std::to_string(r.wires) + " wires)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "set-connector-type") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||||
|
Part *p = resolve_part(m, a[1]); if (!p) return false;
|
||||||
|
SetConnectorTypeResult r = set_connector_type(p, a[2]);
|
||||||
|
if (!r.ok) { emit("set-connector-type refused: " + r.error); return false; }
|
||||||
|
emit(m->name + "/" + p->name + ": connector_type = "
|
||||||
|
+ (a[2].empty() ? "(none)" : a[2]));
|
||||||
|
if (r.materialised > 0)
|
||||||
|
emit("set-connector-type: added " + std::to_string(r.materialised)
|
||||||
|
+ " NC pin(s) from the connector layout");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "set-signal-type") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||||
|
Signal *sig;
|
||||||
|
try { sig = m->signals->get(a[1]); }
|
||||||
|
catch (const std::exception &) {
|
||||||
|
emit("unknown signal: " + m->name + "/" + a[1]); return false;
|
||||||
|
}
|
||||||
|
SetSignalTypeResult r = set_signal_type(sig, a[2]);
|
||||||
|
if (!r.ok) { emit(r.error); return false; }
|
||||||
|
emit(m->name + "/" + sig->name + ": signal type = " + signal_type_name(r.type));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "attach-bsdl") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||||
|
Part *p = resolve_part(m, a[1]); if (!p) return false;
|
||||||
|
AttachBsdlResult r = attach_bsdl(p, a[2]);
|
||||||
|
if (!r.ok) { emit("attach-bsdl: " + r.error); return false; }
|
||||||
|
emit(m->name + "/" + p->name + ": attached BSDL '" + r.entity + "' — "
|
||||||
|
+ std::to_string(r.bound) + "/" + std::to_string(r.ports_total)
|
||||||
|
+ " ports bound" + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "verify") {
|
||||||
|
render_verify();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "export") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
ExportFormat fmt;
|
||||||
|
if (!export_format_from_path(a[0], fmt)) {
|
||||||
|
emit("export: unknown extension (use .csv or .ods): " + a[0]); return false;
|
||||||
|
}
|
||||||
|
ExportResult r = export_connections(sys_.get(), a[0], fmt);
|
||||||
|
if (!r.ok) { emit("export failed: " + r.error); return false; }
|
||||||
|
emit("exported " + std::to_string(r.rows) + " row(s) to " + a[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "save") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
std::string err;
|
||||||
|
if (!save_system(sys_.get(), a[0], err)) { emit("save failed: " + err); return false; }
|
||||||
|
emit("saved to " + a[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "restore") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
std::string err;
|
||||||
|
System *fresh = restore_system(a[0], err);
|
||||||
|
if (!fresh) { emit("restore failed: " + err); return false; }
|
||||||
|
sys_.reset(fresh);
|
||||||
|
emit("restored from " + a[0] + " ("
|
||||||
|
+ std::to_string(sys_->modules()->size()) + " module(s), "
|
||||||
|
+ std::to_string(sys_->connections()->size()) + " connection(s))");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "source") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
bool opened;
|
||||||
|
int n = run_file(a[0], depth + 1, top, opened);
|
||||||
|
if (!opened) { emit("source: cannot open " + a[0]); return false; }
|
||||||
|
emit("source: " + a[0] + " (" + std::to_string(n) + " line(s))");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("script: unsupported command '" + cmd + "'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<System> &sys_;
|
||||||
|
std::ostream &out_;
|
||||||
|
std::map<std::string, std::string> vars_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScriptResult run_script(std::unique_ptr<System> &sys, const std::string &path,
|
||||||
|
std::ostream &out)
|
||||||
|
{
|
||||||
|
ScriptResult r;
|
||||||
|
Runner runner(sys, out);
|
||||||
|
bool opened = false;
|
||||||
|
int n = runner.run_file(path, 0, r, opened);
|
||||||
|
if (!opened) {
|
||||||
|
r.error = "cannot open " + path;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
r.ok = true;
|
||||||
|
r.lines = n;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
35
src/core/app/script.hpp
Normal file
35
src/core/app/script.hpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#ifndef _APP_SCRIPT_HPP_
|
||||||
|
#define _APP_SCRIPT_HPP_
|
||||||
|
|
||||||
|
#include <iosfwd>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
// Application layer: a frontend-agnostic runner for essim command scripts.
|
||||||
|
// Dispatches the scriptable, system-building commands to the app::* operations
|
||||||
|
// and writes their output to a stream — no console, no dialogs, no FTXUI. Any
|
||||||
|
// frontend (and batch mode) can drive a script through it.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Outcome of running a script file.
|
||||||
|
struct ScriptResult {
|
||||||
|
bool ok = false; ///< the top-level file opened and ran
|
||||||
|
std::string error; ///< set when the top-level file can't be opened
|
||||||
|
int lines = 0; ///< effective (non-comment, non-blank) lines run
|
||||||
|
int errors = 0; ///< commands that failed / were unsupported
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the script at `path` against `sys`, writing per-command output to `out`.
|
||||||
|
// `sys` is taken by reference because `new` / `restore` replace the System.
|
||||||
|
// Supported: # comments, blank lines, set + $var/${var} expansion, new, load,
|
||||||
|
// connect, set-connector-type, set-signal-type, attach-bsdl, verify, export,
|
||||||
|
// save, restore, source (nested). Unsupported commands are reported and counted
|
||||||
|
// in `errors`, and execution continues.
|
||||||
|
ScriptResult run_script(std::unique_ptr<System> &sys, const std::string &path,
|
||||||
|
std::ostream &out);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_SCRIPT_HPP_
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "core/app/edit.hpp"
|
#include "core/app/edit.hpp"
|
||||||
#include "core/app/export.hpp"
|
#include "core/app/export.hpp"
|
||||||
#include "core/app/load.hpp"
|
#include "core/app/load.hpp"
|
||||||
|
#include "core/app/script.hpp"
|
||||||
#include "core/app/verify.hpp"
|
#include "core/app/verify.hpp"
|
||||||
#include "core/domain/analysis.hpp"
|
#include "core/domain/analysis.hpp"
|
||||||
#include "core/domain/connect.hpp"
|
#include "core/domain/connect.hpp"
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ namespace {
|
|||||||
enum {
|
enum {
|
||||||
ID_LOAD = wxID_HIGHEST + 1,
|
ID_LOAD = wxID_HIGHEST + 1,
|
||||||
ID_RESTORE,
|
ID_RESTORE,
|
||||||
|
ID_RUN_SCRIPT,
|
||||||
ID_SAVE,
|
ID_SAVE,
|
||||||
ID_EXPORT,
|
ID_EXPORT,
|
||||||
ID_SET_CONNECTOR_TYPE,
|
ID_SET_CONNECTOR_TYPE,
|
||||||
@@ -116,6 +119,7 @@ EssimFrame::EssimFrame(WxFrontend &fe)
|
|||||||
auto *file = new wxMenu;
|
auto *file = new wxMenu;
|
||||||
file->Append(ID_LOAD, "&Load module…\tCtrl-L");
|
file->Append(ID_LOAD, "&Load module…\tCtrl-L");
|
||||||
file->Append(ID_RESTORE, "&Restore snapshot…\tCtrl-R");
|
file->Append(ID_RESTORE, "&Restore snapshot…\tCtrl-R");
|
||||||
|
file->Append(ID_RUN_SCRIPT, "&Run script…\tCtrl-U");
|
||||||
file->Append(ID_SAVE, "&Save snapshot…\tCtrl-S");
|
file->Append(ID_SAVE, "&Save snapshot…\tCtrl-S");
|
||||||
file->AppendSeparator();
|
file->AppendSeparator();
|
||||||
file->Append(ID_EXPORT, "&Export connections…\tCtrl-E");
|
file->Append(ID_EXPORT, "&Export connections…\tCtrl-E");
|
||||||
@@ -169,6 +173,7 @@ EssimFrame::EssimFrame(WxFrontend &fe)
|
|||||||
Bind(wxEVT_MENU, &EssimFrame::OnLoad, this, ID_LOAD);
|
Bind(wxEVT_MENU, &EssimFrame::OnLoad, this, ID_LOAD);
|
||||||
Bind(wxEVT_MENU, &EssimFrame::OnRestore, this, ID_RESTORE);
|
Bind(wxEVT_MENU, &EssimFrame::OnRestore, this, ID_RESTORE);
|
||||||
Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE);
|
Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnRunScript, this, ID_RUN_SCRIPT);
|
||||||
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::OnAttachBsdl, this, ID_ATTACH_BSDL);
|
||||||
@@ -350,6 +355,32 @@ void EssimFrame::OnRestore(wxCommandEvent &) {
|
|||||||
RebuildModelView();
|
RebuildModelView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnRunScript(wxCommandEvent &) {
|
||||||
|
wxFileDialog dlg(this, "Run an essim script", "", "",
|
||||||
|
"essim scripts (*.essim)|*.essim|All files (*.*)|*.*",
|
||||||
|
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
|
||||||
|
fe_.ensure_system();
|
||||||
|
std::ostringstream out;
|
||||||
|
app::ScriptResult r =
|
||||||
|
app::run_script(fe_.system_ptr(), dlg.GetPath().utf8_string(), out);
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
Log("run script: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Run script", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echo each line of the script's output into the log pane.
|
||||||
|
std::istringstream lines(out.str());
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(lines, line)) Log(wx(line));
|
||||||
|
Log(wxString::Format("source: %s (%d line(s), %d error(s))",
|
||||||
|
dlg.GetPath(), r.lines, r.errors));
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
void EssimFrame::OnSave(wxCommandEvent &) {
|
void EssimFrame::OnSave(wxCommandEvent &) {
|
||||||
wxFileDialog dlg(this, "Save system snapshot", "", "system.essim",
|
wxFileDialog dlg(this, "Save system snapshot", "", "system.essim",
|
||||||
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
|
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ private:
|
|||||||
// Menu handlers — each is a thin wrapper over a core/app operation.
|
// Menu handlers — each is a thin wrapper over a core/app operation.
|
||||||
void OnLoad(wxCommandEvent &);
|
void OnLoad(wxCommandEvent &);
|
||||||
void OnRestore(wxCommandEvent &);
|
void OnRestore(wxCommandEvent &);
|
||||||
|
void OnRunScript(wxCommandEvent &);
|
||||||
void OnSave(wxCommandEvent &);
|
void OnSave(wxCommandEvent &);
|
||||||
void OnExport(wxCommandEvent &);
|
void OnExport(wxCommandEvent &);
|
||||||
void OnSetConnectorType(wxCommandEvent &);
|
void OnSetConnectorType(wxCommandEvent &);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public:
|
|||||||
|
|
||||||
// --- used by the window (EssimFrame) ---
|
// --- used by the window (EssimFrame) ---
|
||||||
System *system() const { return sys_.get(); }
|
System *system() const { return sys_.get(); }
|
||||||
|
std::unique_ptr<System> &system_ptr() { return sys_; } ///< for run_script (new/restore replace it)
|
||||||
void set_system(System *fresh); ///< take ownership (used by Restore)
|
void set_system(System *fresh); ///< take ownership (used by Restore)
|
||||||
void ensure_system(); ///< create an empty System if none yet
|
void ensure_system(); ///< create an empty System if none yet
|
||||||
|
|
||||||
|
|||||||
82
tests/test_script.cpp
Normal file
82
tests/test_script.cpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "core/app/script.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// app::run_script is pure core: it drives the app::* operations from a command
|
||||||
|
// script and writes their output to a stream. No UI.
|
||||||
|
|
||||||
|
TEST_CASE("run_script builds a system from a command script") {
|
||||||
|
const char *net = "test_script_in.net";
|
||||||
|
{
|
||||||
|
std::ofstream f(net);
|
||||||
|
f << "COMP: 'C1' 'J1'\n"
|
||||||
|
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||||
|
" Explicit Pin: '2' 'x' 'NETB'\n"
|
||||||
|
"COMP: 'C2' 'J2'\n"
|
||||||
|
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||||
|
" Explicit Pin: '2' 'x' 'NETB'\n";
|
||||||
|
}
|
||||||
|
const char *scr = "test_script_in.essim";
|
||||||
|
{
|
||||||
|
std::ofstream f(scr);
|
||||||
|
f << "# a comment\n"
|
||||||
|
"\n"
|
||||||
|
"set NET " << net << "\n"
|
||||||
|
"new\n"
|
||||||
|
"load M $NET mentor\n"
|
||||||
|
"set-signal-type M NETA power\n"
|
||||||
|
"verify\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<System> sys;
|
||||||
|
std::ostringstream out;
|
||||||
|
app::ScriptResult r = app::run_script(sys, scr, out);
|
||||||
|
|
||||||
|
CHECK(r.ok);
|
||||||
|
CHECK(r.errors == 0);
|
||||||
|
REQUIRE(sys != nullptr);
|
||||||
|
CHECK(sys->modules()->size() == 1);
|
||||||
|
|
||||||
|
const std::string log = out.str();
|
||||||
|
CHECK(log.find("loaded 'M'") != std::string::npos); // load ran
|
||||||
|
CHECK(log.find("signal type = power") != std::string::npos); // $NET expanded + set
|
||||||
|
CHECK(log.find("verify:") != std::string::npos); // verify ran
|
||||||
|
|
||||||
|
std::remove(net);
|
||||||
|
std::remove(scr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("run_script reports a missing top-level file") {
|
||||||
|
std::unique_ptr<System> sys;
|
||||||
|
std::ostringstream out;
|
||||||
|
app::ScriptResult r =
|
||||||
|
app::run_script(sys, "/nonexistent-xyz/none.essim", out);
|
||||||
|
CHECK_FALSE(r.ok);
|
||||||
|
CHECK(r.error.find("cannot open") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("run_script flags an unsupported command and keeps going") {
|
||||||
|
const char *scr = "test_script_unsup.essim";
|
||||||
|
{
|
||||||
|
std::ofstream f(scr);
|
||||||
|
f << "new\nfrobnicate widgets\nnew\n";
|
||||||
|
}
|
||||||
|
std::unique_ptr<System> sys;
|
||||||
|
std::ostringstream out;
|
||||||
|
app::ScriptResult r = app::run_script(sys, scr, out);
|
||||||
|
|
||||||
|
CHECK(r.ok);
|
||||||
|
CHECK(r.errors == 1);
|
||||||
|
CHECK(out.str().find("unsupported command 'frobnicate'") != std::string::npos);
|
||||||
|
REQUIRE(sys != nullptr); // the two `new` lines still ran
|
||||||
|
|
||||||
|
std::remove(scr);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user