Files
playground/pda/tests/session/driver.cpp
T
paulhorn db41258d16 feat: PDA als libpda + pda nach P1204R0
Ersetzt das Geruest (counter/notebook) durch ein echtes Beispiel und teilt
es in zwei eigenstaendige Projekte, wie P1204R0 es fuer Bibliothek plus
Programm verlangt.

libpda: textfile (C, Datei-I/O und Feld-Escaping), calculator
(Recursive-Descent-Parser mit std::expected), contact, editor, explorer.
Exportiert pda::pda ueber install(EXPORT) und ist per find_package(pda)
benutzbar.

pda: Shell ohne Ein-/Ausgabe (execute() gibt Text zurueck, deshalb ohne
Terminal testbar), REPL und Stapelbetrieb in main.cpp.

examples/consumer und tools/check-install.sh pruefen die Export-Kette
Ende-zu-Ende: installieren, dann ein fremdes Projekt dagegen bauen.

docs/CMAKE.md erklaert das Target-Modell, PUBLIC/PRIVATE/INTERFACE,
Generator-Ausdruecke, install/export und Symbolsichtbarkeit an diesem
Projekt.

71 Tests gruen unter Homebrew-clang 22, Apple clang und GCC 16, statisch
und shared, mit und ohne ASan/UBSan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 16:39:35 +02:00

172 lines
5.7 KiB
C++

/* tests/session/driver.cpp
* Integrationstest der Anwendung.
*
* Spielt eine vollstaendige Sitzung durch -- dieselbe Folge von Kommandos,
* die ein Benutzer eintippen wuerde. Was hier gruen ist, funktioniert im
* Programm.
*/
#include <cstdio>
#include <exception>
#include <filesystem>
#include <string>
#include <vector>
#include <pda/shell.hpp>
#define CHECK(cond) \
do \
{ \
if (!(cond)) \
{ \
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
return 1; \
} \
} while (0)
namespace fs = std::filesystem;
namespace
{
/* Fuehrt eine Zeile aus und verlangt, dass sie GELINGT. Gibt die Meldung aus,
* wenn nicht -- sonst sucht man den Fehler in der falschen Zeile. */
bool
run_ok(pda::Shell& shell, const std::string& line)
{
const pda::CommandResult result = shell.execute(line);
if (result.failed)
{
std::fprintf(stderr, " Kommando '%s' scheiterte: %s\n", line.c_str(),
result.output.c_str());
return false;
}
return true;
}
int
a_full_session_from_empty_to_saved(const fs::path& dir)
{
pda::Shell shell{dir};
/* Leerer Anfang. */
CHECK(shell.execute("contact list").output == "keine Kontakte");
CHECK(shell.execute("note list").output == "Notizpuffer leer");
/* Kontakte anlegen -- mit Leerzeichen im Namen und in der Notiz. */
CHECK(run_ok(shell, R"(contact add "Anna Schmidt" 0151 anna@example.org meine Schwester)"));
CHECK(run_ok(shell, R"(contact add "Bert Meier" 0170 bert@firma.de Kollege)"));
CHECK(shell.contacts().size() == 2U);
/* Suchen. */
CHECK(shell.execute("contact find schmidt").output.contains("Anna"));
CHECK(shell.execute("contact find zzz").output == "nichts gefunden");
/* Rechnen zwischendurch. */
CHECK(shell.execute("calc 12 * 12").output == "144");
/* Notizen. */
CHECK(run_ok(shell, "note add Anna anrufen"));
CHECK(run_ok(shell, "note add Rechnung bezahlen"));
CHECK(shell.notes().line_count() == 2U);
CHECK(run_ok(shell, "note del 1"));
CHECK(shell.notes().line_count() == 1U);
CHECK(*shell.notes().line(1U) == "Rechnung bezahlen");
/* Sichern. */
const std::string contacts_file = (dir / "kontakte.tsv").string();
const std::string notes_file = (dir / "notizen.txt").string();
CHECK(run_ok(shell, "contact save " + contacts_file));
CHECK(run_ok(shell, "note save " + notes_file));
/* Eine zweite Sitzung muss dasselbe wiederfinden. */
pda::Shell restored{dir};
CHECK(run_ok(restored, "contact load " + contacts_file));
CHECK(run_ok(restored, "note load " + notes_file));
CHECK(restored.contacts().size() == 2U);
CHECK(restored.contacts().find("Anna Schmidt")->note == "meine Schwester");
CHECK(restored.notes().line_count() == 1U);
/* Der Explorer sieht beide Dateien. */
const pda::CommandResult listing = restored.execute("ls");
CHECK(!listing.failed);
CHECK(listing.output.contains("kontakte.tsv"));
CHECK(listing.output.contains("notizen.txt"));
return 0;
}
int
errors_do_not_corrupt_the_session(const fs::path& dir)
{
pda::Shell shell{dir};
CHECK(run_ok(shell, "contact add Anna 0151 a@b.c"));
/* Eine Reihe von Fehlern -- danach muss der Zustand unveraendert sein. */
const std::vector<std::string> broken{"fliegen",
"calc 1 / 0",
"calc )(",
"contact add Anna",
"note del abc",
"cd gibtesnicht",
"contact load /nix/da.tsv"};
for (const std::string& line : broken)
{
const pda::CommandResult result = shell.execute(line);
if (!result.failed)
{
std::fprintf(stderr, " '%s' haette scheitern muessen\n", line.c_str());
return 1;
}
}
CHECK(shell.contacts().size() == 1U);
CHECK(shell.notes().empty());
CHECK(shell.files().current() == dir);
return 0;
}
} /* namespace */
int
main()
{
/* main darf nichts nach aussen werfen: eine entwichene Exception waere
* ein std::terminate, und CTest zeigte nur "Subprocess aborted" ohne
* jeden Hinweis, welcher Fall gescheitert ist. */
try
{
const fs::path dir = fs::current_path() / "integration.session.tmp";
std::error_code ec;
fs::remove_all(dir, ec);
fs::create_directories(dir, ec);
if (ec)
{
std::fprintf(stderr, "FAIL: Arbeitsverzeichnis nicht anzulegen\n");
return 1;
}
int failures = 0;
failures += a_full_session_from_empty_to_saved(dir);
failures += errors_do_not_corrupt_the_session(dir);
fs::remove_all(dir, ec);
if (failures != 0) return 1;
std::puts("integration/session: ok");
return 0;
}
catch (const std::exception& error)
{
std::fprintf(stderr, "FAIL: unerwartete Exception: %s\n", error.what());
return 1;
}
}