core/ui: extract export into src/app (frontend-agnostic), thin TUI command
First step of separating business logic from the TUI. The export command built
the CSV/ODS file inside its lambda, mixed with Print/ShowError/dialog calls.
Move all of it — CSV + ODS building, sheet-name sanitising, file writing — into
src/app/export.{hpp,cpp} (namespace app, no FTXUI/console dependency):
export_connections(const System*, path, format) -> ExportResult. The TUI
command is now a thin wrapper (resolve args/dialog, call the core, render). The
core is unit-tested without any UI (test_export); 342 assertions pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
190
src/app/export.cpp
Normal file
190
src/app/export.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
#include "app/export.hpp"
|
||||
|
||||
#include "imports/ods_writer.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signal_type.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string to_lower(std::string s)
|
||||
{
|
||||
for (char &c : s) c = (char)std::tolower((unsigned char)c);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Minimal CSV quoter — wraps in "…" and doubles internal quotes when the field
|
||||
// contains a comma, quote, or newline.
|
||||
std::string csv_quote(const std::string &s)
|
||||
{
|
||||
if (s.find_first_of(",\"\n") == std::string::npos)
|
||||
return s;
|
||||
std::string out = "\"";
|
||||
for (char c : s) { if (c == '"') out += '"'; out += c; }
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
// Flatten one pin into the string slots an export row uses.
|
||||
void pin_side(Pin *p, std::string &mod, std::string &part, std::string &pin,
|
||||
std::string &sig, std::string &type, std::string &suspect)
|
||||
{
|
||||
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
|
||||
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
|
||||
part = p->prnt ? p->prnt->name : "";
|
||||
pin = p->name;
|
||||
Signal *s = p->signal();
|
||||
if (!s) {
|
||||
sig = ""; type = "(NC)"; suspect = "";
|
||||
} else {
|
||||
sig = s->name;
|
||||
type = signal_type_name(s->type);
|
||||
suspect = (infer_signal_type(s->name) == SignalType::Power
|
||||
&& s->type == SignalType::Other) ? "yes" : "no";
|
||||
}
|
||||
}
|
||||
|
||||
// Excel rejects /\?*:[] in sheet names; ODS forbids < > & in raw table names.
|
||||
// Sanitise to underscores and clip to Excel's 31-char hard limit.
|
||||
std::string sanitise_sheet_name(std::string name)
|
||||
{
|
||||
for (char &ch : name)
|
||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*' || ch == ':'
|
||||
|| ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&')
|
||||
ch = '_';
|
||||
if (name.size() > 31) name = name.substr(0, 31);
|
||||
return name;
|
||||
}
|
||||
|
||||
ExportResult write_ods(const System *sys, const std::string &path)
|
||||
{
|
||||
ExportResult r;
|
||||
OdsWriter w;
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
OdsSheet *s = w.add_sheet(sanitise_sheet_name(c->name));
|
||||
|
||||
std::string lmod = c->m1 ? c->m1->name : "";
|
||||
std::string lprt = c->p1 ? c->p1->name : "";
|
||||
std::string rmod = c->m2 ? c->m2->name : "";
|
||||
std::string rprt = c->p2 ? c->p2->name : "";
|
||||
|
||||
// Meta header: label/value rows, a blank row, then the column headers.
|
||||
auto meta = [&](int row, const std::string &k, const std::string &v) {
|
||||
s->set(row, 0, k);
|
||||
s->set(row, 1, v);
|
||||
};
|
||||
meta(0, "Connection", c->name);
|
||||
meta(1, "Transform", c->transform_name);
|
||||
meta(2, "Left", lmod + " / " + lprt);
|
||||
meta(3, "Right", rmod + " / " + rprt);
|
||||
// Row 4 left blank by design.
|
||||
|
||||
const int HDR = 5;
|
||||
s->set_header_row(HDR);
|
||||
const char *hdr[] = {
|
||||
"left_pin", "left_signal", "left_type", "left_suspect",
|
||||
"right_pin", "right_signal", "right_type", "right_suspect",
|
||||
"type_mismatch"};
|
||||
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
|
||||
|
||||
int row = HDR + 1;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
|
||||
s->set(row, 0, ln); s->set(row, 1, ls);
|
||||
s->set(row, 2, lt); s->set(row, 3, lsus);
|
||||
s->set(row, 4, rn); s->set(row, 5, rs);
|
||||
s->set(row, 6, rt); s->set(row, 7, rsus);
|
||||
s->set(row, 8, tm);
|
||||
++row; ++r.rows;
|
||||
}
|
||||
}
|
||||
|
||||
std::string err;
|
||||
if (!w.save(path, err)) {
|
||||
r.error = err;
|
||||
return r;
|
||||
}
|
||||
r.ok = true;
|
||||
r.sheets = (int)sys->connections()->size();
|
||||
return r;
|
||||
}
|
||||
|
||||
ExportResult write_csv(const System *sys, const std::string &path)
|
||||
{
|
||||
ExportResult r;
|
||||
std::ofstream f(path);
|
||||
if (!f) {
|
||||
r.error = "cannot open '" + path + "' for writing";
|
||||
return r;
|
||||
}
|
||||
|
||||
// One rectangular table: each row repeats the per-connection constants so
|
||||
// the file stays parser-friendly (pandas / awk / spreadsheet).
|
||||
f << "connection,transform,"
|
||||
"left_module,left_part,"
|
||||
"left_pin,left_signal,left_type,left_suspect,"
|
||||
"right_module,right_part,"
|
||||
"right_pin,right_signal,right_type,right_suspect,"
|
||||
"type_mismatch\n";
|
||||
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
|
||||
f << csv_quote(c->name) << ','
|
||||
<< csv_quote(c->transform_name) << ','
|
||||
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
|
||||
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
|
||||
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
||||
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
|
||||
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
|
||||
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
||||
<< tm << '\n';
|
||||
++r.rows;
|
||||
}
|
||||
}
|
||||
r.ok = f.good();
|
||||
if (!r.ok) r.error = "write error on '" + path + "'";
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool export_format_from_path(const std::string &path, ExportFormat &out)
|
||||
{
|
||||
size_t dot = path.rfind('.');
|
||||
std::string ext = (dot == std::string::npos) ? "" : to_lower(path.substr(dot));
|
||||
if (ext == ".csv") { out = ExportFormat::Csv; return true; }
|
||||
if (ext == ".ods") { out = ExportFormat::Ods; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
ExportResult export_connections(const System *sys, const std::string &path,
|
||||
ExportFormat format)
|
||||
{
|
||||
ExportResult r;
|
||||
if (!sys) { r.error = "no system"; return r; }
|
||||
return (format == ExportFormat::Ods) ? write_ods(sys, path)
|
||||
: write_csv(sys, path);
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
34
src/app/export.hpp
Normal file
34
src/app/export.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef _APP_EXPORT_HPP_
|
||||
#define _APP_EXPORT_HPP_
|
||||
|
||||
#include <string>
|
||||
|
||||
class System;
|
||||
|
||||
// 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 {
|
||||
|
||||
enum class ExportFormat { Csv, Ods };
|
||||
|
||||
// Outcome of an export. The only side effect is writing the target file; the
|
||||
// caller renders `error` / the stats however it likes.
|
||||
struct ExportResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< human-readable, set when !ok
|
||||
int sheets = 0; ///< ODS: number of sheets (one per connection); 0 for CSV
|
||||
int rows = 0; ///< wires written
|
||||
};
|
||||
|
||||
// Map a filename extension (.csv / .ods, case-insensitive) to a format.
|
||||
// Returns false if the extension is neither.
|
||||
bool export_format_from_path(const std::string &path, ExportFormat &out);
|
||||
|
||||
// Export the system's connections to `path` in `format`. Builds the file and
|
||||
// returns stats or an error. Pure core — safe to call from any frontend.
|
||||
ExportResult export_connections(const System *sys, const std::string &path,
|
||||
ExportFormat format);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_EXPORT_HPP_
|
||||
@@ -1,64 +1,25 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "imports/ods_writer.hpp"
|
||||
#include "app/export.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signal_type.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// Minimal CSV quoter — wraps in `"…"` and doubles internal quotes when
|
||||
// the field contains a comma, quote, or newline. Local to this file.
|
||||
std::string csv_quote(const std::string &s) {
|
||||
bool needs = s.find_first_of(",\"\n") != std::string::npos;
|
||||
if (!needs) return s;
|
||||
std::string out = "\"";
|
||||
for (char c : s) { if (c == '"') out += '"'; out += c; }
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
// Flatten one pin into the 6 string slots the export row uses.
|
||||
void pin_side(Pin *p, std::string &mod, std::string &part,
|
||||
std::string &pin, std::string &sig,
|
||||
std::string &type, std::string &suspect) {
|
||||
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
|
||||
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
|
||||
part = p->prnt ? p->prnt->name : "";
|
||||
pin = p->name;
|
||||
Signal *s = p->signal();
|
||||
if (!s) {
|
||||
sig = ""; type = "(NC)"; suspect = "";
|
||||
} else {
|
||||
sig = s->name;
|
||||
type = signal_type_name(s->type);
|
||||
suspect = (infer_signal_type(s->name) == SignalType::Power
|
||||
&& s->type == SignalType::Other) ? "yes" : "no";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Thin UI wrapper around app::export_connections — this file only resolves
|
||||
// arguments / the file dialog and renders the result. All the actual export
|
||||
// (CSV / ODS building, file writing) lives in src/app/export.cpp.
|
||||
void Tui::RegisterExportCommands() {
|
||||
commands["export"] = {
|
||||
{{"kind [connections]", Completion::None},
|
||||
{"filename (.csv)", Completion::Path}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
|
||||
if (args.empty()) {
|
||||
// Bare → reuse the generic file dialog. Filters give a
|
||||
// one-keystroke CSV/ODS toggle; picking either rewrites
|
||||
// the filename's extension, and the action below
|
||||
// dispatches on that extension.
|
||||
// Bare → the generic file dialog. The CSV/ODS filter rewrites
|
||||
// the extension; the action below dispatches on it.
|
||||
OpenFileDialog(
|
||||
"Export — connections",
|
||||
"export.connections",
|
||||
@@ -76,155 +37,29 @@ void Tui::RegisterExportCommands() {
|
||||
const std::string &kind = args[0];
|
||||
const std::string &path = args[1];
|
||||
|
||||
if (kind == "connections") {
|
||||
// Accepted extensions: `.csv` (flat file) and `.ods`
|
||||
// (one sheet per connection). Anything else is an error.
|
||||
std::string ext;
|
||||
{
|
||||
size_t dot = path.rfind('.');
|
||||
if (dot != std::string::npos) ext = ToLower(path.substr(dot));
|
||||
}
|
||||
bool ods = (ext == ".ods");
|
||||
bool csv = (ext == ".csv");
|
||||
if (!ods && !csv) {
|
||||
ShowError("export: unknown extension '"
|
||||
+ (ext.empty() ? std::string("(none)") : ext)
|
||||
+ "'\nAccepted: .csv, .ods");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ods) {
|
||||
OdsWriter w;
|
||||
int total = 0;
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
// Sheet names: Excel rejects /\?*:[] characters,
|
||||
// ODS forbids < > & in raw cell/table names.
|
||||
// Sanitise to underscores; clip to 31 chars
|
||||
// (Excel's hard limit).
|
||||
std::string sname = c->name;
|
||||
for (char &ch : sname)
|
||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
||||
|| ch == ':' || ch == '[' || ch == ']'
|
||||
|| ch == '<' || ch == '>' || ch == '&') ch = '_';
|
||||
if (sname.size() > 31) sname = sname.substr(0, 31);
|
||||
OdsSheet *s = w.add_sheet(sname);
|
||||
|
||||
// Pull the constants for this connection once.
|
||||
// `transform`, the left module/part, and the
|
||||
// right module/part don't vary across the wires
|
||||
// of a single connection — putting them in
|
||||
// every row was repetitive.
|
||||
std::string lmod, lprt;
|
||||
std::string rmod, rprt;
|
||||
if (c->m1) lmod = c->m1->name;
|
||||
if (c->p1) lprt = c->p1->name;
|
||||
if (c->m2) rmod = c->m2->name;
|
||||
if (c->p2) rprt = c->p2->name;
|
||||
|
||||
// Meta header above the table: 5 rows of label /
|
||||
// value, then a blank, then the column headers
|
||||
// on row 6 (index 5).
|
||||
auto meta = [&](int r, const std::string &k,
|
||||
const std::string &v) {
|
||||
s->set(r, 0, k);
|
||||
s->set(r, 1, v);
|
||||
};
|
||||
meta(0, "Connection", c->name);
|
||||
meta(1, "Transform", c->transform_name);
|
||||
meta(2, "Left", lmod + " / " + lprt);
|
||||
meta(3, "Right", rmod + " / " + rprt);
|
||||
// Row 4 left blank by design.
|
||||
|
||||
const int HDR = 5;
|
||||
s->set_header_row(HDR);
|
||||
const char *hdr[] = {
|
||||
"left_pin", "left_signal", "left_type", "left_suspect",
|
||||
"right_pin", "right_signal", "right_type", "right_suspect",
|
||||
"type_mismatch"};
|
||||
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
|
||||
|
||||
int row = HDR + 1;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
// `type_mismatch = yes` when both sides have a
|
||||
// real signal AND their types disagree (e.g.
|
||||
// Power ↔ Gnd, or Power ↔ Other).
|
||||
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt)
|
||||
? "yes" : "no";
|
||||
s->set(row, 0, ln); s->set(row, 1, ls);
|
||||
s->set(row, 2, lt); s->set(row, 3, lsus);
|
||||
s->set(row, 4, rn); s->set(row, 5, rs);
|
||||
s->set(row, 6, rt); s->set(row, 7, rsus);
|
||||
s->set(row, 8, tm);
|
||||
++row; ++total;
|
||||
}
|
||||
}
|
||||
std::string err;
|
||||
if (!w.save(path, err)) {
|
||||
ShowError("export (.ods) failed:\n" + err);
|
||||
return;
|
||||
}
|
||||
Print("export connections (.ods): "
|
||||
+ std::to_string(sys->connections()->size())
|
||||
+ " sheet(s), " + std::to_string(total)
|
||||
+ " wire(s) → " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Classic flat CSV: a single rectangular table — one
|
||||
// header line, N data rows, every row carries the per-
|
||||
// connection constants too. The 9 right-most column
|
||||
// names match the ODS sheet headers exactly; the 5
|
||||
// leading ones (connection, transform, left_module,
|
||||
// left_part, right_module, right_part) correspond to
|
||||
// the ODS meta block (Connection, Transform, Left,
|
||||
// Right). Repeating the constants per row keeps the
|
||||
// file parser-friendly (pandas / awk / spreadsheet).
|
||||
std::ofstream f(path);
|
||||
if (!f) {
|
||||
ShowError("export: cannot open '" + path + "' for writing");
|
||||
return;
|
||||
}
|
||||
f << "connection,transform,"
|
||||
"left_module,left_part,"
|
||||
"left_pin,left_signal,left_type,left_suspect,"
|
||||
"right_module,right_part,"
|
||||
"right_pin,right_signal,right_type,right_suspect,"
|
||||
"type_mismatch\n";
|
||||
|
||||
int rows = 0;
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
std::string tm = "no";
|
||||
if (lt != "(NC)" && rt != "(NC)" && lt != rt) tm = "yes";
|
||||
f << csv_quote(c->name) << ','
|
||||
<< csv_quote(c->transform_name) << ','
|
||||
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
|
||||
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
|
||||
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
||||
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
|
||||
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
|
||||
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
||||
<< tm << '\n';
|
||||
++rows;
|
||||
}
|
||||
}
|
||||
Print("export connections (.csv): " + std::to_string(rows)
|
||||
+ " wire(s) → " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind != "connections") {
|
||||
ShowError("export: unknown kind '" + kind + "'\n"
|
||||
"Known kinds: connections");
|
||||
return;
|
||||
}
|
||||
|
||||
app::ExportFormat fmt;
|
||||
if (!app::export_format_from_path(path, fmt)) {
|
||||
ShowError("export: unknown extension — accepted: .csv, .ods");
|
||||
return;
|
||||
}
|
||||
|
||||
app::ExportResult r = app::export_connections(sys.get(), path, fmt);
|
||||
if (!r.ok) {
|
||||
ShowError("export failed:\n" + r.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool ods = (fmt == app::ExportFormat::Ods);
|
||||
std::string msg = ods ? "export connections (.ods): "
|
||||
: "export connections (.csv): ";
|
||||
if (ods) msg += std::to_string(r.sheets) + " sheet(s), ";
|
||||
Print(msg + std::to_string(r.rows) + " wire(s) → " + path);
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
"export structured data to CSV / ODS (kinds: connections; "
|
||||
|
||||
76
tests/test_export.cpp
Normal file
76
tests/test_export.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "app/export.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string slurp(const std::string &path)
|
||||
{
|
||||
std::ifstream f(path);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("export_format_from_path maps extensions") {
|
||||
app::ExportFormat f;
|
||||
CHECK(app::export_format_from_path("a.csv", f));
|
||||
CHECK(f == app::ExportFormat::Csv);
|
||||
CHECK(app::export_format_from_path("a.ODS", f));
|
||||
CHECK(f == app::ExportFormat::Ods);
|
||||
CHECK_FALSE(app::export_format_from_path("a.txt", f));
|
||||
CHECK_FALSE(app::export_format_from_path("noext", f));
|
||||
}
|
||||
|
||||
TEST_CASE("export_connections writes a flat CSV (no UI needed)") {
|
||||
// Two cards, one wired pin pair via a connection.
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Module *b = sys.modules()->merge("B");
|
||||
Part *ja = new Part("J1"); a->add(ja);
|
||||
Part *jb = new Part("P1"); b->add(jb);
|
||||
Pin *pa = new Pin("1"); ja->add(pa);
|
||||
Pin *pb = new Pin("1"); jb->add(pb);
|
||||
Signal *sa = a->signals->merge("NETA"); sa->add(pa); pa->connect(sa);
|
||||
Signal *sb = b->signals->merge("NETB"); sb->add(pb); pb->connect(sb);
|
||||
|
||||
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
|
||||
c->transform_name = "identity";
|
||||
c->pin_map.emplace_back(pa, pb);
|
||||
sys.connections()->add(c);
|
||||
|
||||
const char *path = "test_export_out.csv";
|
||||
app::ExportResult r = app::export_connections(&sys, path, app::ExportFormat::Csv);
|
||||
CHECK(r.ok);
|
||||
CHECK(r.rows == 1);
|
||||
|
||||
std::string out = slurp(path);
|
||||
CHECK(out.find("connection,transform,") == 0); // header present
|
||||
CHECK(out.find("A.J1<->B.P1") != std::string::npos); // connection name
|
||||
CHECK(out.find("identity") != std::string::npos); // transform
|
||||
CHECK(out.find("NETA") != std::string::npos); // left signal
|
||||
CHECK(out.find("NETB") != std::string::npos); // right signal
|
||||
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
TEST_CASE("export_connections reports a bad path instead of crashing") {
|
||||
System sys;
|
||||
app::ExportResult r = app::export_connections(
|
||||
&sys, "/nonexistent-dir-xyz/out.csv", app::ExportFormat::Csv);
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK_FALSE(r.error.empty());
|
||||
}
|
||||
Reference in New Issue
Block a user