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:
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