db41258d16
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>
172 lines
5.7 KiB
C++
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;
|
|
}
|
|
}
|