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>
157 lines
4.8 KiB
C++
157 lines
4.8 KiB
C++
/* tests/basics/driver.cpp
|
|
* Integrationstest der Bibliothek (P1204R0 Regel 7.2).
|
|
*
|
|
* Kein Test-Framework: ein Integrationstest soll ein eigenstaendiges Programm
|
|
* sein, das ohne Argumente laeuft und ueber seinen Exit-Code Auskunft gibt.
|
|
* Er benutzt nur <libpda/...> -- genau das, was ein Benutzer bekommt.
|
|
*
|
|
* Der Test bildet einen kleinen echten Ablauf ab: Kontakte anlegen, sichern,
|
|
* in einem zweiten Buch laden, Notizen schreiben und ueber den Explorer
|
|
* nachsehen, dass die Dateien wirklich da sind.
|
|
*/
|
|
#include <cstdio>
|
|
#include <exception>
|
|
#include <filesystem>
|
|
#include <string>
|
|
|
|
#include <libpda/calculator.hpp>
|
|
#include <libpda/contact.hpp>
|
|
#include <libpda/editor.hpp>
|
|
#include <libpda/explorer.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
|
|
{
|
|
|
|
int
|
|
contacts_survive_a_save_and_load_cycle(const fs::path& dir)
|
|
{
|
|
const fs::path file = dir / "kontakte.tsv";
|
|
|
|
pda::ContactBook book;
|
|
CHECK(book.add(pda::Contact{"Anna Schmidt", "0151", "anna@example.org", "Schwester"}));
|
|
CHECK(book.add(pda::Contact{"Bert Meier", "0170", "bert@firma.de", "Kollege\tAbt. 4"}));
|
|
CHECK(book.size() == 2U);
|
|
|
|
CHECK(book.save(file).has_value());
|
|
|
|
pda::ContactBook reloaded;
|
|
CHECK(reloaded.load(file).has_value());
|
|
CHECK(reloaded.size() == 2U);
|
|
|
|
const pda::Contact* bert = reloaded.find("Bert Meier");
|
|
CHECK(bert != nullptr);
|
|
|
|
/* Das TAB in der Notiz ist der Grund fuer das Escaping in textfile.c --
|
|
* ohne das haette der Datensatz beim Laden fuenf Felder statt vier. */
|
|
CHECK(bert->note == "Kollege\tAbt. 4");
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
notes_round_trip_through_a_file(const fs::path& dir)
|
|
{
|
|
const fs::path file = dir / "notizen.txt";
|
|
|
|
pda::TextBuffer buffer;
|
|
buffer.append("Einkaufen");
|
|
buffer.append("Zahnarzt anrufen");
|
|
CHECK(buffer.modified());
|
|
|
|
CHECK(buffer.save(file).has_value());
|
|
CHECK(!buffer.modified());
|
|
|
|
pda::TextBuffer reloaded;
|
|
CHECK(reloaded.load(file).has_value());
|
|
CHECK(reloaded.line_count() == 2U);
|
|
CHECK(*reloaded.line(2U) == "Zahnarzt anrufen");
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
the_explorer_sees_what_we_wrote(const fs::path& dir)
|
|
{
|
|
const pda::Explorer explorer{dir};
|
|
|
|
const auto entries = explorer.list();
|
|
CHECK(entries.has_value());
|
|
CHECK(entries->size() == 2U);
|
|
|
|
/* Alphabetisch: kontakte.tsv vor notizen.txt. */
|
|
CHECK((*entries)[0].name == "kontakte.tsv");
|
|
CHECK((*entries)[1].name == "notizen.txt");
|
|
CHECK((*entries)[0].size > 0U);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
the_calculator_agrees_with_arithmetic()
|
|
{
|
|
const auto value = pda::evaluate("(2 + 3) * 4 - 10 / 2");
|
|
CHECK(value.has_value());
|
|
CHECK(*value == 15.0);
|
|
|
|
const auto broken = pda::evaluate("2 +");
|
|
CHECK(!broken.has_value());
|
|
CHECK(!pda::format_error(broken.error()).empty());
|
|
|
|
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
|
|
{
|
|
/* Eigenes Arbeitsverzeichnis, damit der Test nichts findet, was er nicht
|
|
* selbst angelegt hat -- und nichts hinterlaesst. */
|
|
const fs::path dir = fs::current_path() / "integration.basics.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 += contacts_survive_a_save_and_load_cycle(dir);
|
|
failures += notes_round_trip_through_a_file(dir);
|
|
failures += the_explorer_sees_what_we_wrote(dir);
|
|
failures += the_calculator_agrees_with_arithmetic();
|
|
|
|
fs::remove_all(dir, ec);
|
|
|
|
if (failures != 0) return 1;
|
|
|
|
std::puts("integration/basics: ok");
|
|
return 0;
|
|
}
|
|
catch (const std::exception& error)
|
|
{
|
|
std::fprintf(stderr, "FAIL: unerwartete Exception: %s\n", error.what());
|
|
return 1;
|
|
}
|
|
}
|