A könyvtári mag és a parancssori program közötti határ akkor hasznos, ha nem pusztán két forrásfájlt választ el, hanem két külön szerződést is. A mag bájtsorozatot értelmez, állapotot tart fenn és értékként visszaadható eredményt készít. A CLI fájlt nyit, olvasási hibát kezel, jelentést formáz, majd kilépési kóddal jelzi a művelet eredményét.
A Projektkövetelmény, protokoll és ownership
térkép kanonikus projektszerződését itt valósítjuk meg: ugyanazt a
rekordnyelvtant, CLI-felületet, kimeneti formátumot és
kilépésikód-rendszert. Az önállóan elkészíthető laborhoz a szükséges
részleteket megismételjük; eltérés esetén a 17.1 szerződése az irányadó.
A TCP-adapter és a 39. fejezet C++ portja
ugyanezt a könyvtári interfészt használja: a parser nem ismer sem
FILE objektumot, sem file descriptort, sem hálózati
kapcsolatot.
A bemenet nulla vagy több, LF-fel lezárt rekordból áll:
név=érték\n
A \n itt egyetlen, 0x0a értékű oktettet
jelöl, nem két nyomtatható karaktert.
| Elem | Szerződés |
|---|---|
| Név | 1–16 ASCII kisbetű, kizárólag a–z |
| Elválasztó | Egyetlen = |
| Érték | 1–5 decimális számjegy, értéke legfeljebb 65535 |
| Vezető nullák | Megengedettek, de beleszámítanak az öt számjegybe |
| Rekordlezárás | Kötelező LF; CRLF nem elfogadott |
| Üres bemenet | Érvényes, nulla rekorddal |
| Maximális bemenet | 1 048 576 C bájt |
| Ismétlődő név | Megengedett; minden rekord külön számít |
| Eredmény | Rekordszám, értékösszeg, elfogadott bájtok száma |
A parser nem tárolja el a neveket: ez a változat a rekordfolyam szerkezeti érvényességét és összesített számait vizsgálja, nem metrikanevekből épít adatbázist.
Nyelvi és platformbesorolás. A könyvtár ISO C17
nyelvi és könyvtári eszközöket használ, 8 bites C bájtot, megfelelő
méretű size_t típust és ASCII-kompatibilis
karakterkészletet feltételezve. A CLI Linux/POSIX kiegészítést használ a
SIGPIPE kezelésére. Nem használunk exact-width egész
típust, így például a uint64_t elérhetősége nem
előfeltétel.
A labor profilja a központi profiltábla
lab-gcc profilja: x86-64 Linux, System V ABI, LP64, GCC
13.3.0, glibc 2.39, GNU binutils 2.42, explicit -std=c17,
GNU nyelvi bővítések nélkül. A buildszkriptek clang ága a
lab-clang profilhoz tartozik. A labor buildjét és tesztjeit
a lab-gcc profilban lefuttattuk; az eredményeket az
alfejezet végi buildjegyzőkönyv rögzíti. A saját laborban a tényleges
fordító-, libc- és eszközverziókat a központi tábla mintájára kell
rögzíteni.
A használat:
diag [FILE|-]
Argumentum nélkül vagy - argumentummal a program a
standard bemenetet olvassa. Siker esetén a standard kimeneten pontosan
egy sor jelenik meg:
OK records=2 sum=19 bytes=11
A mezősorrend, az ASCII szóközök és a záró LF a szerződés részei.
| Kilépési kód | Jelentés | Például |
|---|---|---|
| 0 | Teljes siker | A jelentés kiírása és flush művelete is sikerült |
| 2 | Hibás CLI-használat | Túl sok argumentum |
| 3 | Bemeneti I/O-hiba | Megnyitás, olvasás vagy saját fájl lezárása |
| 4 | Hibás bemenet | Szintaxis, csonkolás vagy méretlimit |
| 5 | Erőforrás- vagy belső állapothiba | Allokáció vagy jelkezelési beállítás |
| 6 | Jelentésírási hiba | Sikertelen formázott kiírás vagy flush |
Parserhiba esetén a standard hibakimenet formátuma:
ERR code=truncated offset=10
Más hibákhoz nem kapcsolunk értelmetlen bájtoffsetet, például:
ERR code=open
A hibakimenet írása best effort: hibás stderr
esetén üzenetet nem tudunk garantálni. A kilépési kódok a program rendes
vezérlési útjaira vonatkoznak, nem például a folyamat külső,
kényszerített megszüntetésére.
A projekt elrendezése:
include/
diag.h
src/
diag.c
main.c
tests/
test_diag.c
spec_examples.c
cli_args.sh
golden.in
golden.out
truncated.in
truncated.err
build/
Az opaque parser olyan típus, amelynek deklarációja publikus, objektumreprezentációja viszont csak a megvalósításban látható. A kliens ezért nem tudja közvetlenül elrontani például az aktuális számjegyszámot. A határ nem teszi automatikusan örökké ABI-kompatibilissé a teljes API-t, de elrejti a parser belső méretét és elrendezését.
| Erőforrás vagy adat | Tulajdonos | Élettartam, érvénytelenítés |
|---|---|---|
| Parserobjektum | CLI vagy teszt | Létrehozástól dg_destroy hívásig |
| Allokátor-context | Az API hívója | A parser megsemmisítéséig élnie kell |
| Olvasópuffer | CLI | Automatikus tároló a main végéig |
dg_view által jelölt adat |
A hívó | A dg_feed hívás alatt olvasható; a parser nem őrzi
meg |
Másolt dg_error |
A másolatot tároló hívó | Független a parser és a bemenet későbbi élettartamától |
Saját megnyitott FILE |
CLI | Pontosan egyszer lezárandó |
stdin, stdout, stderr |
Futási környezet | A CLI kölcsönzött hozzáféréssel használja |
Miért nincs arena? Az arena közös élettartamú objektumcsoportok egyszerű felszabadítására alkalmas. Itt azonban nem őrzünk rekordokat vagy neveket: a parser egyetlen, állandó méretű objektum. Egy arena bevezetése csak további állapotot és hibalehetőséget teremtene.
A byte view nem tulajdonló, hosszával együtt átadott adatszelet. Nem nullterminált sztring, ezért beágyazott nullbájt esetén sem rövidül meg „magától”: a parser azt szintaktikai hibaként látja.
include/diag.hA következő fájl önállóan include-olható C17 header. Nem teszi szükségessé, hogy a kliens előtte más headert is beillesszen.
#ifndef DIAG_H
#define DIAG_H
#include <stdbool.h>
#include <stddef.h>
#define DG_MAX_INPUT ((size_t)1048576UL)
typedef struct dg_parser dg_parser;
typedef struct {
const unsigned char *data;
size_t size;
} dg_view;
typedef enum {
DG_OK = 0,
DG_SYNTAX,
DG_TRUNCATED,
DG_TOO_LARGE,
DG_STATE
} dg_code;
typedef struct {
dg_code code;
size_t offset;
} dg_error;
typedef struct {
size_t bytes;
size_t records;
unsigned long long sum;
} dg_report;
typedef struct {
void *(*allocate)(size_t size, void *context);
void (*release)(void *pointer, void *context);
void *context;
} dg_allocator;
/* NULL allocator selects malloc/free. */
dg_parser *dg_create(const dg_allocator *allocator);
void dg_destroy(dg_parser *parser);
/* Input is borrowed only for the duration of this call. */
bool dg_feed(dg_parser *parser, dg_view input);
/* Declares end of input. Repeated successful finish is allowed. */
bool dg_finish(dg_parser *parser);
/* Returned values do not borrow from parser. */
dg_error dg_get_error(const dg_parser *parser);
dg_report dg_get_report(const dg_parser *parser);
/* Returns a static string; the caller must not release it. */
const char *dg_code_name(dg_code code);
#endifA hívási előfeltételek fontosak:
dg_destroy(NULL) megengedett, más parserparaméternek
élő parserre kell mutatnia;A konstruktor lemásolja az allokátorleírót, de nem másolja le a context által jelölt objektumot. A context élettartama ezért továbbra is a hívó felelőssége.
src/diag.cA parser két fázist különböztet meg: névolvasást és számolvasást. A névolvasás nulla névhosszal egyben rekordhatárt is jelent.
#include "diag.h"
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
_Static_assert(CHAR_BIT == 8, "8-bit C bytes required");
_Static_assert(SIZE_MAX >= 1048577UL, "size_t too narrow");
_Static_assert('a' == 97 && 'z' == 122 &&
'0' == 48 && '9' == 57 &&
'=' == 61 && '\n' == 10,
"ASCII-compatible execution character set required");
enum phase {
PH_KEY = 0,
PH_NUMBER
};
struct dg_parser {
dg_allocator allocator;
dg_report report;
dg_error error;
size_t key_length;
unsigned digits;
unsigned long value;
enum phase phase;
bool finished;
};
static void *default_allocate(size_t size, void *context)
{
(void)context;
return malloc(size);
}
static void default_release(void *pointer, void *context)
{
(void)context;
free(pointer);
}
static bool fail(dg_parser *parser, dg_code code)
{
if (parser->error.code == DG_OK) {
parser->error.code = code;
parser->error.offset = parser->report.bytes;
}
return false;
}
dg_parser *dg_create(const dg_allocator *allocator)
{
dg_allocator chosen = {
default_allocate, default_release, NULL
};
if (allocator != NULL) {
chosen = *allocator;
}
dg_parser *parser =
chosen.allocate(sizeof *parser, chosen.context);
if (parser == NULL) {
return NULL;
}
*parser = (dg_parser){ .allocator = chosen };
return parser;
}
void dg_destroy(dg_parser *parser)
{
if (parser != NULL) {
dg_allocator allocator = parser->allocator;
allocator.release(parser, allocator.context);
}
}
bool dg_feed(dg_parser *parser, dg_view input)
{
if (parser->error.code != DG_OK) {
return false;
}
if (parser->finished) {
return fail(parser, DG_STATE);
}
for (size_t i = 0; i < input.size; ++i) {
if (parser->report.bytes == DG_MAX_INPUT) {
return fail(parser, DG_TOO_LARGE);
}
unsigned char byte = input.data[i];
if (parser->phase == PH_KEY) {
if (byte >= 'a' && byte <= 'z') {
if (parser->key_length == 16) {
return fail(parser, DG_SYNTAX);
}
++parser->key_length;
} else if (byte == '=' && parser->key_length != 0) {
parser->phase = PH_NUMBER;
parser->digits = 0;
parser->value = 0;
} else {
return fail(parser, DG_SYNTAX);
}
} else {
if (byte >= '0' && byte <= '9') {
unsigned long digit = (unsigned long)(byte - '0');
if (parser->digits == 5 ||
parser->value > (65535UL - digit) / 10UL) {
return fail(parser, DG_SYNTAX);
}
parser->value = parser->value * 10UL + digit;
++parser->digits;
} else if (byte == '\n' && parser->digits != 0) {
++parser->report.records;
parser->report.sum += parser->value;
parser->phase = PH_KEY;
parser->key_length = 0;
} else {
return fail(parser, DG_SYNTAX);
}
}
++parser->report.bytes;
}
return true;
}
bool dg_finish(dg_parser *parser)
{
if (parser->error.code != DG_OK) {
return false;
}
if (parser->phase != PH_KEY || parser->key_length != 0) {
return fail(parser, DG_TRUNCATED);
}
parser->finished = true;
return true;
}
dg_error dg_get_error(const dg_parser *parser)
{
return parser->error;
}
dg_report dg_get_report(const dg_parser *parser)
{
return parser->report;
}
const char *dg_code_name(dg_code code)
{
switch (code) {
case DG_OK: return "ok";
case DG_SYNTAX: return "syntax";
case DG_TRUNCATED: return "truncated";
case DG_TOO_LARGE: return "limit";
case DG_STATE: return "state";
}
return "state";
}Az offset nulla alapú. Szintaktikai hibánál az első elutasított bájt
helyét, csonkolásnál a bemenet végét jelöli. A limit túllépésekor az
első már nem engedélyezett bájt offsetje pontosan
DG_MAX_INPUT.
A számláló csak sikeresen feldolgozott bájt után nő. Emiatt a hibát kiváltó bájt helye további korrekció nélkül rendelkezésre áll. A hiba ragadós: az első hiba után minden újabb feed és finish sikertelen, az eredeti hibaadat változatlan marad.
A dg_error nem tartalmaz bemenetbe mutató pointert vagy
ideiglenes formázópuffert. A másolata a parser megsemmisítése után is
használható. A hibakód szöveges neve statikus sztring; azt sem kell
felszabadítani.
A számfeldolgozásnál legyen az eddigi érték, az új számjegy, . A következő érték:
Mivel , a megengedhetőség feltétele:
Ezt a szorzás előtt ellenőrizzük. Nem előbb számolunk egy esetleg túl nagy értéket, hogy utólag vizsgáljuk.
Bizonyításvázlat az összeg korlátjára. A legrövidebb
rekord négy bájt, például a=0\n. Ha
,
akkor legfeljebb
teljes rekord fér el. Így:
Ez belefér az ISO C17 által az unsigned long long
számára garantált minimális értéktartományba. A bizonyítás a rögzített
bemeneti limitre támaszkodik: a limit későbbi emelésekor ezt az érvelést
is felül kell vizsgálni.
A feldolgozás legrosszabb esetben idő és parsermemória. Bájtanként korlátos számú művelet történik, nincs amortizált növekedés vagy rekordfüggő allokáció. A hívás után az inputpuffer azonnal újrahasználható.
A CLI blokkoló, kötegelt feldolgozó. A fread használata
itt nem helyettesíti a későbbi nonblocking állapotgépet.
A ciklus legfeljebb a fennmaradó keret plusz egy bájtját kéri ki a
FILE adatfolyamból. Az extra bájt szükséges ahhoz, hogy
megkülönböztessük a pontosan limitméretű bemenetet a túl hosszútól. Ez
alkalmazási szintű korlát: a standard I/O belső előreolvasása külön
mechanizmus.
src/main.cA fájl C17 és Linux/POSIX környezetet igényel. A buildparancsban szereplő feature-test makró teszi elérhetővé a használt POSIX deklarációkat.
#include "diag.h"
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
static void complain(const char *code)
{
(void)fprintf(stderr, "ERR code=%s\n", code);
}
static int parser_failure(const dg_parser *parser)
{
dg_error error = dg_get_error(parser);
(void)fprintf(stderr, "ERR code=%s offset=%zu\n",
dg_code_name(error.code), error.offset);
return error.code == DG_STATE ? 5 : 4;
}
int main(int argc, char **argv)
{
int status = 0;
FILE *input = stdin;
bool own_input = false;
dg_parser *parser = NULL;
dg_report report = {0};
size_t seen = 0;
unsigned char buffer[4096];
struct sigaction action = {0};
action.sa_handler = SIG_IGN;
if (sigemptyset(&action.sa_mask) == -1 ||
sigaction(SIGPIPE, &action, NULL) == -1) {
complain("setup");
status = 5;
goto cleanup;
}
if (argc > 2) {
complain("usage");
status = 2;
goto cleanup;
}
if (argc == 2 && strcmp(argv[1], "-") != 0) {
input = fopen(argv[1], "rb");
if (input == NULL) {
complain("open");
status = 3;
goto cleanup;
}
own_input = true;
}
parser = dg_create(NULL);
if (parser == NULL) {
complain("alloc");
status = 5;
goto cleanup;
}
for (;;) {
size_t remaining = DG_MAX_INPUT - seen + 1;
size_t wanted =
remaining < sizeof buffer ? remaining : sizeof buffer;
size_t count = fread(buffer, 1, wanted, input);
if (ferror(input)) {
complain("read");
status = 3;
goto cleanup;
}
if (count != 0) {
dg_view view = { buffer, count };
if (!dg_feed(parser, view)) {
status = parser_failure(parser);
goto cleanup;
}
seen += count;
}
if (feof(input)) {
break;
}
if (count == 0) {
complain("read");
status = 3;
goto cleanup;
}
}
if (!dg_finish(parser)) {
status = parser_failure(parser);
goto cleanup;
}
report = dg_get_report(parser);
cleanup:
if (own_input) {
if (fclose(input) == EOF && status == 0) {
complain("close");
status = 3;
}
}
dg_destroy(parser);
if (status == 0) {
if (fprintf(stdout,
"OK records=%zu sum=%llu bytes=%zu\n",
report.records, report.sum, report.bytes) < 0) {
complain("write");
status = 6;
}
if (fflush(stdout) == EOF) {
if (status == 0) {
complain("write");
}
status = 6;
}
}
return status;
}A seen változó csak teljesen sikeres feed után változik.
Ekkor legfeljebb a limitet érheti el, ezért a
DG_MAX_INPUT - seen kivonás nem csordul alul.
Az olvasás után előbb az I/O-hibát ellenőrizzük. Ha ugyanaz a
fread részadatot és hibajelzést is eredményez, az I/O-hiba
elsőbbséget kap; a részadatból nem készül sikeres jelentés.
A cleanup ágban a fclose minden saját fájlon lefut,
akkor is, ha már van korábbi hiba. A lezárási hiba csak akkor módosítja
az eredményt, ha addig sikeres volt a feldolgozás. Így egy másodlagos
takarítási hiba nem tünteti el az elsődleges parserhibát.
A jelentés a saját bemenet sikeres lezárása után jelenik meg. A
%zu a size_t, a %llu az
unsigned long long megfelelő formátuma.
Linux/POSIX viselkedés: megszűnt olvasóoldalú pipe
esetén az alapértelmezett SIGPIPE megszakíthatná a
folyamatot a saját hibakezelés előtt. Ezért a CLI figyelmen kívül hagyja
ezt a jelet, és az írás hibáját vizsgálja. A sikeres
fprintf még nem elég: pufferelt kimenetnél a hiba csak
fflush alatt is megjelenhet.
A kimenet nem tranzakciós. Íráshiba után maradhat részleges jelentés a fogadónál; csak a nulla kilépési kód igazolja a teljes sikerességet.
tests/test_diag.cA teszt C17-ben fordítható. A CHECK ellenőrzései
NDEBUG mellett sem tűnnek el.
#include "diag.h"
#include <stdio.h>
#include <stdlib.h>
#define CHECK(condition) do { \
if (!(condition)) { \
(void)fprintf(stderr, "CHECK failed at line %d\n", __LINE__); \
exit(EXIT_FAILURE); \
} \
} while (0)
static void test_splits(void)
{
static const unsigned char input[] = "a=7\ncpu=12\n";
const size_t length = sizeof input - 1;
for (size_t split = 0; split <= length; ++split) {
dg_parser *parser = dg_create(NULL);
CHECK(parser != NULL);
CHECK(dg_feed(parser, (dg_view){ input, split }));
CHECK(dg_feed(parser,
(dg_view){ input + split, length - split }));
CHECK(dg_finish(parser));
CHECK(dg_finish(parser));
dg_report report = dg_get_report(parser);
CHECK(report.records == 2);
CHECK(report.sum == 19);
CHECK(report.bytes == 11);
dg_destroy(parser);
}
}
static void test_errors(void)
{
unsigned char bad[] = "a=7\ncpu=12x\n";
dg_parser *parser = dg_create(NULL);
CHECK(parser != NULL);
CHECK(!dg_feed(parser, (dg_view){ bad, sizeof bad - 1 }));
dg_error saved = dg_get_error(parser);
CHECK(saved.code == DG_SYNTAX);
CHECK(saved.offset == 10);
bad[10] = '\n';
CHECK(!dg_feed(parser, (dg_view){ bad, sizeof bad - 1 }));
CHECK(dg_get_error(parser).offset == saved.offset);
CHECK(!dg_finish(parser));
dg_destroy(parser);
CHECK(saved.code == DG_SYNTAX);
CHECK(saved.offset == 10);
static const unsigned char truncated[] = "a=7\ncpu=12";
parser = dg_create(NULL);
CHECK(parser != NULL);
CHECK(dg_feed(parser,
(dg_view){ truncated, sizeof truncated - 1 }));
CHECK(!dg_finish(parser));
CHECK(dg_get_error(parser).code == DG_TRUNCATED);
CHECK(dg_get_error(parser).offset == 10);
dg_destroy(parser);
}
struct test_heap {
size_t allocations;
size_t releases;
bool fail;
};
static void *test_allocate(size_t size, void *context)
{
struct test_heap *heap = context;
++heap->allocations;
return heap->fail ? NULL : malloc(size);
}
static void test_release(void *pointer, void *context)
{
struct test_heap *heap = context;
++heap->releases;
free(pointer);
}
static void test_allocation(void)
{
struct test_heap heap = {0};
dg_allocator allocator = {
test_allocate, test_release, &heap
};
heap.fail = true;
CHECK(dg_create(&allocator) == NULL);
CHECK(heap.allocations == 1);
CHECK(heap.releases == 0);
heap.fail = false;
dg_parser *parser = dg_create(&allocator);
CHECK(parser != NULL);
static const unsigned char bad[] = "!";
CHECK(!dg_feed(parser, (dg_view){ bad, 1 }));
dg_destroy(parser);
dg_destroy(NULL);
CHECK(heap.allocations == 2);
CHECK(heap.releases == 1);
}
static void test_limit(void)
{
static const unsigned char record[] = "a=0\n";
for (int extra = 0; extra < 2; ++extra) {
dg_parser *parser = dg_create(NULL);
CHECK(parser != NULL);
for (size_t i = 0; i < DG_MAX_INPUT / 4; ++i) {
CHECK(dg_feed(parser, (dg_view){ record, 4 }));
}
CHECK(dg_get_report(parser).bytes == DG_MAX_INPUT);
if (extra == 0) {
CHECK(dg_finish(parser));
} else {
CHECK(!dg_feed(parser, (dg_view){ record, 1 }));
CHECK(dg_get_error(parser).code == DG_TOO_LARGE);
CHECK(dg_get_error(parser).offset == DG_MAX_INPUT);
}
dg_destroy(parser);
}
}
int main(void)
{
test_splits();
test_errors();
test_allocation();
test_limit();
return 0;
}A darabolási teszt a golden bemenet minden két részre bontását
kipróbálja, beleértve az üres első vagy második részt. A
input + length one-past pointer csak nulla hosszúságú
view-ban szerepel, és nem dereferáljuk.
Bizonyításvázlat a darabolásfüggetlenségre. A parser
minden állapotváltozása egy bemeneti bájthoz kötődik; a feed-hívások
határán nincs implicit rekordlezárás vagy állapotnullázás. Ugyanaz a
bájtsorozat ezért ugyanazt az állapotátmenet-sorozatot eredményezi. A
végleges lezárás kizárólag a dg_finish feladata. A teszt
ennek konkrét ellenőrzése, nem minden lehetséges bemenet teljes
bizonyítása.
Az allokációs teszt lefedi a könyvtár egyetlen allokációs pontjának hibáját. Nem vizsgálja a libc belső allokációit, és önmagában nem bizonyítja a teljes CLI minden erőforráshibás útját.
Az alábbi parancsokat a tanuló külön laborban használhatja.
Alapértelmezésben mindkét fordítóval (lab-gcc és
lab-clang profil) épít; ha csak az egyik érhető el, a
COMPILERS változóval szűkíthető, például
COMPILERS=gcc sh build.sh.
set -eu
for cc in ${COMPILERS:-gcc clang}; do
mkdir -p "build/$cc"
"$cc" -std=c17 -O2 -g \
-Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-Iinclude -c src/diag.c -o "build/$cc/diag.o"
ar rcs "build/$cc/libdiag.a" "build/$cc/diag.o"
"$cc" -std=c17 -O2 -g \
-Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-D_POSIX_C_SOURCE=200809L -Iinclude \
src/main.c "build/$cc/libdiag.a" -o "build/$cc/diag"
"$cc" -std=c17 -O2 -g \
-Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-Iinclude tests/test_diag.c "build/$cc/libdiag.a" \
-o "build/$cc/test_diag"
doneA statikus könyvtár a rá hivatkozó forrásból képzett objektum után kerül a linkparancsba. A publikus header önmagában nem tartalmaz definíciót a parser függvényeihez; azokat a könyvtár szolgáltatja.
A fixture fájlokat az alábbi parancsok pontosan, a záró LF jelenlétét is egyértelművé téve állítják elő:
printf 'a=7\ncpu=12\n' > tests/golden.in
printf 'OK records=2 sum=19 bytes=11\n' > tests/golden.out
printf 'a=7\ncpu=12' > tests/truncated.in
printf 'ERR code=truncated offset=10\n' > tests/truncated.errA golden teszt ismert bemenethez előre rögzített, bájtpontosan összehasonlítható kimenetet rendel. A csonkolt változatból csak az utolsó LF hiányzik, de ettől az egész feldolgozás sikertelen: nincs részleges sikerjelentés.
set -eu
for cc in ${COMPILERS:-gcc clang}; do
"build/$cc/test_diag"
"build/$cc/diag" tests/golden.in > "build/$cc/golden.actual"
cmp tests/golden.out "build/$cc/golden.actual"
rc=0
"build/$cc/diag" tests/truncated.in \
> "build/$cc/truncated.out" \
2> "build/$cc/truncated.err" || rc=$?
test "$rc" -eq 4
test ! -s "build/$cc/truncated.out"
cmp tests/truncated.err "build/$cc/truncated.err"
doneA parancssori felület maga is a szerződés része, ezért az argumentumfeldolgozásnak külön regressziós tesztje van. A szkript a kilépési kódot, a teljes standard kimenetet és a teljes hibakimenetet is összeveti a projektszerződés táblázatával:
#!/bin/sh
# tests/cli_args.sh DIAG -- az argumentumfeldolgozás regressziós tesztje
set -u
diag=$1
fail=0
errf=$(mktemp) || exit 2
expect() { # várt_rc várt_stdout várt_stderr parancs...
want_rc=$1 want_out=$2 want_err=$3
shift 3
out=$("$@" 2>"$errf") && rc=0 || rc=$?
err=$(cat "$errf")
if [ "$rc" != "$want_rc" ] || [ "$out" != "$want_out" ] ||
[ "$err" != "$want_err" ]; then
echo "FAIL: $* -> rc=$rc out=[$out] err=[$err]" >&2
fail=1
fi
}
expect 0 'OK records=2 sum=19 bytes=11' '' "$diag" tests/golden.in
expect 0 'OK records=2 sum=19 bytes=11' '' sh -c "\"$diag\" - < tests/golden.in"
expect 0 'OK records=2 sum=19 bytes=11' '' sh -c "\"$diag\" < tests/golden.in"
expect 2 '' 'ERR code=usage' "$diag" tests/golden.in extra
expect 3 '' 'ERR code=open' "$diag" tests/does-not-exist.in
expect 4 '' 'ERR code=truncated offset=10' "$diag" tests/truncated.in
rm -f "$errf"
exit $failFuttatása fordítónként:
sh tests/cli_args.sh build/gcc/diag. Sikeres esetben nem ír
semmit, és 0 kóddal lép ki.
Buildjegyzőkönyv (lab-gcc profil). A
fenti forrásokat, a fixture-öket, a golden- és a csonkolási tesztet, a
tests/cli_args.sh szkriptet és a sanitizeres
könyvtártesztet Linux x86-64 környezetben, GCC 13.3.0, glibc 2.39 és GNU
binutils 2.42 mellett lefuttattuk; a fordítás figyelmeztetés nélküli
volt, minden ellenőrzés sikeres. A sanitizeres buildet GCC-vel
(-fsanitize=address,undefined) futtattuk. Clanggal és a
fent választott GCC 14.2.0-val ebben a jegyzőkönyvben nem fordítottunk;
ezeket a saját laborban kell rögzíteni.
A könyvtár és tesztje külön diagnosztikai buildben is fordítható:
clang -std=c17 -O1 -g -Wall -Wextra -Wpedantic \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-Iinclude src/diag.c tests/test_diag.c \
-o build/test_diag_sanEz fordítási recept, nem sanitizermentes futásról szóló állítás.
| Hibaminta | Miért hibás? | Javítás |
|---|---|---|
strlen használata az olvasópufferen |
Nem garantált a nullterminálás; a nullbájt is bemenet | A fread által visszaadott hossz átadása |
| Rekordlezárás minden feed végén | Az I/O-darabolás nem protokollhatár | Állapot megőrzése a hívások között |
| Offset pointerként való tárolása | A puffer újrahasználata érvényteleníti a tartalmi hivatkozást | Számszerű offset és másolható hibakód |
Sikerjelentés a finish előtt |
A végső rekord csonkolt lehet | Explicit EOF-feldolgozás |
fflush eredményének elhagyása |
A tényleges írás később is hibázhat | Flush és hibakód ellenőrzése |
| A cleanup hibája mindig felülírja az eredményt | Elveszhet az eredeti ok | Elsődleges hiba megőrzése |
A parser megkapja az a=7\ncpu=12 bemenetet. Milyen
jelentést ad vissza közvetlenül a feed után? Mi történik finish esetén,
és miért nem írhatja ki a CLI a prefix jelentését sikeres
eredményként?
Bővítsd a tesztet a következő bemenetekkel:
k=65535\n;k=65536\n;k=000000\n.Ellenőrizd az elfogadást, illetve az első hibás bájt offsetjét. A teszt minden parserobjektumot szabadítson fel.
Linuxon irányítsd a golden bemenet jelentését a
/dev/full eszközre. Milyen kilépési kódot vársz? Miért
lehet hibás az a javítás, amely csak a fprintf eredményét
ellenőrzi? Magyarázd meg azt is, miért nem várunk release
hívást a sikertelen konstruktor után.
A feed sikeres: még nem tudhatja, érkezik-e folytatás. A pillanatnyi jelentés:
bytes = 10;records = 1;sum = 7.A második rekord értéke már feldolgozható volt, de LF nélkül még
nincs teljes rekord. A finish eredménye DG_TRUNCATED,
offsetje 10. Az ellenőrzési feltétel: a CLI 4-es kóddal lép ki, és a
standard kimenete üres. A prefixstatisztika diagnosztikai adat, nem
végleges sikereredmény.
Az alábbi C17 részlet a tesztfájlba illeszthető. Hozzá kell adni a
<string.h> include-ot, és a main
függvényből meg kell hívni a tesztet.
static void test_numeric_boundaries(void)
{
const struct {
const char *input;
dg_code code;
size_t offset;
} cases[] = {
{ "k=65535\n", DG_OK, 0 },
{ "k=65536\n", DG_SYNTAX, 6 },
{ "k=000000\n", DG_SYNTAX, 7 }
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
dg_parser *parser = dg_create(NULL);
CHECK(parser != NULL);
bool accepted = dg_feed(parser, (dg_view){
(const unsigned char *)cases[i].input,
strlen(cases[i].input)
});
if (cases[i].code == DG_OK) {
CHECK(accepted);
CHECK(dg_finish(parser));
CHECK(dg_get_report(parser).sum == 65535);
} else {
CHECK(!accepted);
dg_error error = dg_get_error(parser);
CHECK(error.code == cases[i].code);
CHECK(error.offset == cases[i].offset);
}
dg_destroy(parser);
}
}Itt a strlen helyes: a tesztadat garantáltan
nullterminált sztringliterál, beágyazott nullbájt nélkül. A
65536 utolsó számjegye a 6-os offseten sérti az
értékkorlátot; a hatodik nulla a 7-es offseten sérti a számjegyszám
korlátját.
A /dev/full Linux-specifikus diagnosztikai eszköz. Az
ellenőrzés várt eredménye 6-os kilépési kód és
ERR code=write hibasor, feltéve, hogy a hibakimenet
írható.
rc=0
build/gcc/diag tests/golden.in > /dev/full 2> build/full.err || rc=$?
test "$rc" -eq 6A rövid jelentés elférhet a standard I/O pufferében, ezért a
fprintf sikeres lehet, miközben a fflush
hibázik. Mindkét műveletet ellenőrizni kell.
Sikertelen konstruktor esetén az allokátor null pointert adott vissza: nem keletkezett tulajdonolt parserobjektum. Nincs mit felszabadítani. A teszt elvárt számlálói egy sikertelen, majd egy sikeres konstrukció és annak megsemmisítése után: két allokációs kísérlet, egy felszabadítás.
A könyvtári mag kizárólag állapotot és másolható eredményt birtokol; a bemenetet csak a feed-hívás idejére kölcsönzi. A CLI feladata az I/O, a végleges lezárás, a stabil report és a hibakódok kezelése.
A helyességhez nem elég, hogy egy golden bemenet „jó számokat” ad. A darabolási határok, a csonkolt EOF, a pontos méretlimit, az allokációs hiba és a késleltetett kimeneti hiba ugyanannak a szerződésnek a részei. Ez a világos határ teszi lehetővé, hogy a következő lépésben a könyvtár változtatása nélkül építsünk hozzá nonblocking TCP-adaptert.