39.4 CMake, tesztek és mérési csomag

A port akkor tekinthető késznek, ha a viselkedése nemcsak kézzel kipróbálva, hanem dokumentált módon újra előállítható és ellenőrizhető. Ez az alfejezet ezért nem újabb nyelvi funkciót tanít, hanem a C++ záróprojekt köré épített build-, teszt- és mérési csomagot szerződésként írja le. A funkcionális magot és a szolgáltatás-életciklust a Task, queue és szolgáltatás-életciklus (39.3) rögzítette; a CTest, a negatív tesztek és a sanitizerek általános mintáit a CTest, negatív tesztek és sanitizer CI (38.4) tárgyalta. Itt ezeket a konkrét projektre alkalmazzuk, és kiegészítjük a C/C++ összehasonlítás mérési tervével.

A fejezet állításai eszközök dokumentált lehetőségeire támaszkodnak, de nem jelentenek futtatási eredményt. A megadott parancsok a tanuló laborjában futtatandók; a compiler-, library- és CMake-verziót a projektprofilban kell rögzíteni, mert a konkrét kapcsolók és viselkedésük verziófüggőek. Ahol a szabvány és az implementáció eltérhet, azt külön jelölöm.

39.4.1 A build mint visszajátszható szerződés

A projektprofil (project profile) azoknak a külső feltételeknek a rögzített halmaza, amelyek mellett a build és a teszt értelmezhető: operációs rendszer és architektúra, C- és C++-fordító pontos kiadása, standard library megvalósítása és verziója, CMake-verzió, generátor, build típus, nyelvi standard, optimalizálási szint, sanitizer-kapcsolók és a forrás commitja. A build provenance ennek géppel is olvasható lenyomata a build mellé írva. Nem azonos a bitre reprodukálható builddel: utóbbi azt jelenti, hogy azonos bemenetből bitre azonos artefaktum keletkezik, ami erősebb és külön infrastruktúrát igényel; ezzel a Release, debug symbol és reprodukálhatóság (38.6) foglalkozik. Itt a gyengébb, de napi gyakorlatban elegendő célt követjük: a build visszajátszható és a környezete utólag azonosítható legyen.

A CMake configure lépésében kiírható a provenance. Két részlet könnyen elcsúszik. A compiler azonosítója (CMAKE_CXX_COMPILER_ID) nem azonosítja a standard könyvtárat: a Clang például libstdc++-szal és libc++-szal is dolgozhat. Ezért a könyvtárat egy kis próbaprogram azonosítja, amely a ténylegesen használt fordítási beállításokkal fordul és fut:

// cmake/stdlib_probe.cpp -- a ténylegesen használt C++ standard könyvtár
#include <cstdio>
#include <version>

int main()
{
#if defined(_LIBCPP_VERSION)
    std::printf("libc++ %d", _LIBCPP_VERSION);
#elif defined(__GLIBCXX__)
    std::printf("libstdc++ %d (release %d)", __GLIBCXX__, _GLIBCXX_RELEASE);
#elif defined(_MSVC_STL_VERSION)
    std::printf("msvc-stl %d", _MSVC_STL_VERSION);
#else
    std::printf("unknown");
#endif
}

A másik csapda a konfigurációspecifikus változók neve. Ezek nagybetűsek (CMAKE_CXX_FLAGS_RELWITHDEBINFO), ezért a CMAKE_BUILD_TYPE értékét a lekérdezés előtt nagybetűssé kell alakítani; egyébként a kiírt flag-lista éppen az optimalizálási és NDEBUG-kapcsolókat hagyja ki. Többkonfigurációs generátornál (Ninja Multi-Config, Visual Studio) a konfiguráció a build idején dől el, ezért minden konfigurációhoz külön fájl készül:

# cmake/provenance.cmake -- build provenance konfigurációnként
if(CMAKE_CROSSCOMPILING)
  set(diag_cxx_stdlib "unknown (cross-compiling, nem futtatható próba)")
else()
  try_run(diag_probe_run diag_probe_compiled
    SOURCES "${CMAKE_CURRENT_LIST_DIR}/stdlib_probe.cpp"
    CXX_STANDARD 20
    RUN_OUTPUT_VARIABLE diag_cxx_stdlib)
  if(NOT diag_probe_compiled OR NOT diag_probe_run EQUAL 0)
    set(diag_cxx_stdlib "unknown (a próba nem futott)")
  endif()
endif()

get_property(diag_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(diag_multi)
  set(diag_configs ${CMAKE_CONFIGURATION_TYPES})
else()
  set(diag_configs "${CMAKE_BUILD_TYPE}")
endif()
if(NOT diag_configs)
  set(diag_configs None)          # nincs konfigurációspecifikus flag
endif()

foreach(cfg IN LISTS diag_configs)
  string(TOUPPER "${cfg}" CFG)    # a változónév nagybetűs: _RELWITHDEBINFO
  file(WRITE "${CMAKE_BINARY_DIR}/build-provenance-${cfg}.txt"
"project=${PROJECT_NAME}
source_dir=${CMAKE_SOURCE_DIR}
cmake=${CMAKE_VERSION}
generator=${CMAKE_GENERATOR}
config=${cfg}
c_compiler=${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}
cxx_compiler=${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}
cxx_stdlib=${diag_cxx_stdlib}
system=${CMAKE_SYSTEM_NAME}
processor=${CMAKE_SYSTEM_PROCESSOR}
c_flags=${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${CFG}}
cxx_flags=${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${CFG}}
")
endforeach()

A gyökér CMakeLists.txt a project() után include(cmake/provenance.cmake) sorral veszi fel. Egykonfigurációs generátornál a fájl neve például build-provenance-RelWithDebInfo.txt; konfiguráció nélkül build-provenance-None.txt, üres konfigurációs flag-listával. Keresztfordításnál a próba nem futtatható; ilyenkor a mező ezt jelzi, és a könyvtárat a toolchain-fájlból kell rögzíteni.

Buildjegyzőkönyv (lab-gcc profil). A mintát CMake 4.4.4 és GCC 13.3.0 mellett egy- és többkonfigurációs Ninja generátorral is lefuttattuk. A RelWithDebInfo provenance cxx_flags mezője (-O2 -g -DNDEBUG) megegyezett a compile_commands.json fordítási parancsának kapcsolóival; a cxx_stdlib mező értéke libstdc++ 20240904 (release 13) volt. A Ninja Multi-Config generátor a Debug, Release és RelWithDebInfo konfigurációhoz három külön fájlt írt, a megfelelő flagekkel. Clanggal, így a libc++-os ágat, nem futtattuk.

Ez a fájl configure időben keletkezik, ezért a ténylegesen használt flag-eket a CMake változóiból veszi. Ha a projekt külső változóból (például környezeti változóból) módosítja a flag-eket, vagy targetenként ad hozzá kapcsolókat, azt is rögzíteni kell; a fenti minta csak a standard CMake-változókat fedi le, a target-szintű target_compile_options értékeit a compile_commands.json mellé mentésével lehet megőrizni. A provenance-t a tesztriport mellé kell menteni, különben egy későbbi hibaelemzésnél nem tudjuk megmondani, melyik fordítóval és milyen optimalizálással született az eredmény.

A buildparancsok a projekt gyökeréből:

cmake -S . -B build -G Ninja \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
cmake --build build --parallel
ctest --test-dir build --output-on-failure

A RelWithDebInfo és a Release konfiguráció NDEBUG-ot definiál, ezért a tesztek ellenőrzései nem épülhetnek assert-re; a projekt tesztjei ezért saját, mindig lefutó CHECK makrót használnak (lásd a CMake capstone targetjeit). A RelWithDebInfo a méréshez és a hibakereséshez is használható kompromisszum, de a teljesítményméréshez külön, rögzített Release profilt kell használni, és a sanitizeres futásokat külön buildkönyvtárban kell tartani. A C és C++ fordítót nem elég a CC/CXX környezeti változóra bízni: a CMake cache a első configure után megőrzi a választást, ezért a profilban explicit módon adjuk meg.

39.4.2 Target-gráf és fordítási kapuk

A target-based CMake lényege, hogy a fordítási beállítások ahhoz a targethez tartoznak, amelynek a kódjára vonatkoznak, és nem globális változóként terjednek szét. A warning kapu külön interfész targetbe kerül:

add_library(diag_warnings INTERFACE)

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  target_compile_options(diag_warnings INTERFACE
    -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror)
else()
  message(WARNING "Ismeretlen compiler: a warning kapu nem aktív")
endif()

A -Wconversion hasznos a méret- és előjeles konverziók megfogására, de zajos lehet generált és harmadik féltől származó fejlécekben. Ezért a saját targetek linkeljék a diag_warnings-t, a külső függőségek fejléceit pedig SYSTEM include-ként kell felvenni, hogy a warningok ne a mi kapunkat terheljék. A -Werror csak akkor véd, ha a figyelmeztetéskészlet a rögzített compilerverzióhoz kötött; új compiler új figyelmeztetéseket hozhat, ezért a warning kaput a projektprofil részeként kell kezelni, nem örök érvényű szabályként.

A target-gráf a portban három réteget tartalmaz: a C++ magot (diagcore_core, forrása src/diag_core.cpp), a stabil C API-t (diagcore_c_api, forrása src/diag_c_api.cpp, nyilvános fejléce a változatlan include/diag.h) és a futtatható klienst (diagcore_cli, a 17.2 C17-ben fordított src/main.c fájlja). A forrásokat és a megfigyelhető viselkedést a portterv és a C++ core rögzíti; a CLI felülete a projektszerződés szerinti diag [FILE|-]. A C API target csak a C++ magra linkel, a C kliensek csak a C API-ra. Ez a linkhatár az, amit a Stabil shared library határ és verziózás (36.3) szerződése rögzít. A warning targetet mindhárom saját target linkeli, de a C API fejlécét C fordítóval is le kell fordítani, különben a extern "C"-hibák rejtve maradnak.

39.4.3 CTest-tesztkategóriák és a golden corpus

A tesztkészletet érdemes címkékkel (CTest LABELS) kategóriákra bontani, mert a gyors és a lassú teszteket más-más gyakorisággal futtatjuk. Négy kategória elegendő: unit (egyedi függvények, értéktípusok), golden (rögzített bemenet–kimenet párok), negative (dokumentált elutasítás) és abi (C API/FFI smoke). A fuzz és a mérés külön, nem CTest-vezérelt folyamat.

A golden corpus olyan változatlan bemeneti fájlok és elvárt kimenetek halmaza, amelyekhez a parser és a szolgáltatás viselkedését hasonlítjuk. A golden teszt akkor használható, ha a kimenet determinisztikus, vagy ha a nem determinisztikus részeket (időbélyeg, PID, allokációs cím, sorrend) normalizáljuk. A normalizálás nem csalás, hanem a szerződés része: rögzíteni kell, mely mezők számítanak és melyek nem.

add_executable(diagcore_tests tests/test_core.cpp)
target_link_libraries(diagcore_tests PRIVATE diagcore_core diag_warnings)

add_test(NAME unit_core COMMAND diagcore_tests)
set_tests_properties(unit_core PROPERTIES LABELS "unit")

add_executable(spec_examples tests/spec_examples.c)
target_link_libraries(spec_examples PRIVATE diagcore_c_api)
add_test(NAME spec_examples COMMAND spec_examples)
set_tests_properties(spec_examples PROPERTIES LABELS "golden")

# Golden, csonkolt és használati hibás esetek: kilépési kód,
# teljes stdout és teljes stderr összevetése (lásd 17.2).
add_test(NAME cli_contract
  COMMAND sh tests/cli_args.sh $<TARGET_FILE:diagcore_cli>
  WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
set_tests_properties(cli_contract PROPERTIES LABELS "golden;negative")

A tesztforrások a projektszerződés, a 17.2 labor és a C++ core fájljai. A cli_args.sh a golden bemenetet, a csonkolt bemenetet, a túl sok argumentumot és a nem létező fájlt is lefuttatja, és mindegyiknél a pontos kilépési kódot, stdoutot és stderrt ellenőrzi; a negatív esetek ezért nem WILL_FAIL-re épülnek. A C és C++ CLI differenciális összevetése (portterv, tests/diff_c_cxx.sh) a C referenciabuild targetjét is igényli, ezért külön CI-lépés.

A WILL_FAIL önmagában csak azt ellenőrzi, hogy a folyamat nem nulla visszatérési kóddal áll le. Nem bizonyítja, hogy a helyes hibakódot adta, és nem zárja ki, hogy összeomlás miatt tért vissza hibásan. Ezért a negatív teszthez olyan tesztprogram kell, amely a dokumentált hibakódot is ellenőrzi; a fenti cli_contract ilyen. A golden teszteknél ugyanez a helyzet: a fájlösszehasonlítás akkor jó, ha a normalizálás és az elvárt fájl verziója is a repóban van.

39.4.4 Parser fuzz és allokációs hiba

A fuzz célja itt nem a helyes parse bizonyítása, hanem a memóriabiztonság és a terminálás: tetszőleges byte-sorozatra a parser vagy elfogad, vagy dokumentált hibával elutasít, de nem olvas ki a pufferből, nem függeszti fel a szálat, és nem fogyaszt korlátlan memóriát. A libFuzzer target csak Clang alatt és külön buildben kapcsolható be:

option(DIAGCORE_ENABLE_FUZZING "libFuzzer target" OFF)

if(DIAGCORE_ENABLE_FUZZING)
  add_executable(diagcore_parser_fuzz fuzz/parser_fuzz.cpp)
  target_link_libraries(diagcore_parser_fuzz PRIVATE diagcore_core)
  target_compile_options(diagcore_parser_fuzz PRIVATE
    -fsanitize=fuzzer,address -fno-omit-frame-pointer)
  target_link_options(diagcore_parser_fuzz PRIVATE
    -fsanitize=fuzzer,address)
endif()

A fuzz target a LLVMFuzzerTestOneInput belépési pontot használja, és a corpus könyvtárat is át kell adni neki. A futtatás:

cmake -S . -B build-fuzz -G Ninja \
  -DCMAKE_CXX_COMPILER=clang++ \
  -DDIAGCORE_ENABLE_FUZZING=ON \
  -DCMAKE_BUILD_TYPE=Debug
cmake --build build-fuzz --parallel
./build-fuzz/diagcore_parser_fuzz -max_total_time=60 corpus/

Az allokációs hiba tesztelése azt ellenőrzi, hogy a kód minden allokációs ponton képes visszajelzni a hibát, nem hagy maga után lezáratlan erőforrást, és nem hagy félkész objektumot. A port core-ja nem allokál; a C API egyetlen allokációs pontja a dg_create, amelynek hibáját a hívó dg_allocator callbackjével injektáljuk (lásd az FFI smoke tesztet lent). A dokumentált eredmény: a dg_create NULL-t ad, a CLI pedig ERR code=alloc diagnosztikával, 5-ös kilépési kóddal áll le. Ha a platformadapter std::pmr tárolót használ, ott egy olyan std::pmr::memory_resource jól használható, amely adott számú allokáció után std::bad_alloc-ot dob:

#include <cstddef>
#include <memory_resource>
#include <new>

struct FailingResource final : std::pmr::memory_resource {
    std::size_t remaining;
    explicit FailingResource(std::size_t n) : remaining(n) {}

    void* do_allocate(std::size_t bytes, std::size_t align) override {
        if (remaining == 0) throw std::bad_alloc{};
        --remaining;
        return std::pmr::new_delete_resource()->allocate(bytes, align);
    }

    void do_deallocate(void* p, std::size_t bytes, std::size_t align) override {
        std::pmr::new_delete_resource()->deallocate(p, bytes, align);
    }

    bool do_is_equal(
        const std::pmr::memory_resource& other) const noexcept override {
        return this == &other;
    }
};

A teszt a FailingResource-ot úgy állítja be, hogy az első néhány allokáció sikerüljön, majd a következő dobjon. Ezután ellenőrizni kell, hogy a hívó a dokumentált hibát kapja, a destruktorok lefutottak, és a C API határán nem szivárgott ki C++ kivétel. Új C hibakód, például egy NOMEM érték bevezetése megfigyelhető változás lenne, ezért csak külön migrációként jöhetne szóba; a kanonikus szerződésben az allokációs hiba jelzése a NULL handle.

39.4.5 Sanitizer-mátrix

A sanitizereket nem egyetlen buildben kell futtatni. Az AddressSanitizer és az UndefinedBehaviorSanitizer kombinálható, a ThreadSanitizer viszont külön buildet igényel, mert az ASan és a TSan memóriahozzáférési modellje nem kompatibilis. A mátrix célja, hogy minden hibamódhoz legyen legalább egy futás, és a futások eredménye a provenance-szel együtt maradjon meg.

Build Kapcsoló Mit fed Mit nem fed
build-asan -fsanitize=address,undefined buffer túlcsordulás, use-after-free, néhány UB adatverseny, logikai hiba
build-tsan -fsanitize=thread adatverseny, szinkronizációs hiba memóriahiba, UB
build-debug nincs sanitizer teljes debugger-láthatóság rejtett memóriahiba
build-release nincs sanitizer teljesítmény és bináris méret a legtöbb memóriahiba

A sanitizeres futás nem helyettesíti sem a determinisztikus tesztet, sem a kódolvasást. Az ASan nem találja meg a nem tűnt hibát, a TSan pedig csak a ténylegesen megtörtént, dinamikusan észlelt versenyhelyzeteket jelzi; egy ritkán előforduló logikai race rejtve maradhat. Ezért a TSan-t a szálas szolgáltatás-életciklus és a queue tesztjein kell futtatni, ismételt és terhelt forgatókönyvvel, nem egyetlen rövid futással.

39.4.6 C API kompatibilitás és FFI smoke test

A C API akkor kompatibilis, ha a fejlécet C fordító is elfogadja, a szimbólumok neve és hívási konvenciója a dokumentált ABI szerint alakul, és a C++ kivételek nem lépnek át a határon. A minimális ellenőrzés egy C nyelven fordított kliens, amely a nyilvános fejlécet include-olja és linkel a könyvtárhoz:

/* tests/c_api_smoke.c -- C fordítóval fordított kliens a C++ könyvtárhoz */
#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 *refuse(size_t size, void *context)
{
    (void)size;
    ++*(int *)context;
    return NULL;                    /* allokációs hiba injektálása */
}

static void never(void *pointer, void *context)
{
    (void)pointer;
    (void)context;
    abort();                        /* sikertelen create nem szabadít */
}

int main(void)
{
    static const unsigned char payload[] = "cpu=42\n";

    dg_parser *p = dg_create(NULL);
    CHECK(p != NULL);
    CHECK(dg_feed(p, (dg_view){ payload, sizeof payload - 1 }));
    CHECK(dg_finish(p));
    CHECK(dg_get_report(p).records == 1 && dg_get_report(p).sum == 42);
    dg_destroy(p);

    int calls = 0;
    dg_allocator failing = { refuse, never, &calls };
    CHECK(dg_create(&failing) == NULL);
    CHECK(calls == 1);
    return 0;
}
add_executable(c_api_smoke tests/c_api_smoke.c)
target_link_libraries(c_api_smoke PRIVATE diagcore_c_api diag_warnings)
set_target_properties(c_api_smoke PROPERTIES C_STANDARD 17 C_EXTENSIONS OFF)

A lényeg, hogy ezt a targetet C fordító fordítsa, ne C++ fordító. Ha C++ fordítóval fordítjuk, a extern "C" hiánya nem derül ki, és a teszt hamis biztonságot ad. Az FFI smoke teszt nem teljes ABI-kompatibilitási teszt; a verziózott szimbólumok, a struct-layout és a régi binárisokkal való kompatibilitás ellenőrzése az ABI- és FFI-labor, kompatibilitási teszt (36.5) feladata. Itt csak a határ átjárhatóságát igazoljuk. A tesztet GCC 13.3.0-val C17-ben fordítottuk, és a C++ könyvtárhoz (g++ driverrel), valamint ellenőrzésképpen a C referenciakönyvtárhoz is linkelve lefuttattuk, mindkét esetben sikerrel, a C++ változatot ASan/LSan/UBSan builddel is; az nm a C++ könyvtárban a dg_* szimbólumokat név-dekoráció nélkül, C kötéssel mutatta. A CMake-fragmentumokat ebben a jegyzőkönyvben nem futtattuk, mert CMake nem állt rendelkezésre.

39.4.7 A mérési terv

A mérés DBEN nem az a kérdés, hogy „gyorsabb-e a C++”, hanem hogy a port ugyanazon a workloadon milyen költséget mutat, és ez hogyan viszonyul a rögzített baseline-hoz. A workload a feldolgozandó bemenet és a futtatási mód együttese; a metrika a mért mennyiség és mértékegysége; a mérési terv rögzíti a kettőt, az ismétlésszámot, a statisztikát és az elfogadási feltételt.

Metrika Eszköz Egység Kontroll
allokációk száma számláló allocator vagy pmr resource darab/futás azonos bemenet, azonos élettartam
csúcs memória /usr/bin/time -v vagy getrusage KiB külön folyamat, hideg indítás
latency medián/p95 saját harness, monotonic óra µs warmup után, 30 ismétlés
throughput feldolgozott byte / falóidő MB/s azonos corpus, azonos párhuzamosság
bináris méret size, stat byte stripped és unstripped külön
build idő time cmake --build s tiszta buildkönyvtár

A mérési harnessnek ugyanazt a publikus API-t kell hívnia, mint a valós kliensnek, különben a mérés a benchmarkra optimalizált mellékutat mér. A bemenetet fájlból vagy memóriából kell betölteni, és a betöltés idejét külön kell mérni vagy ki kell zárni. A statisztikát előre rögzíteni kell: például medián és p95, nem pedig a legjobb futás. A warmup szükséges, mert az első futás tartalmazza a page faultokat, a dinamikus loader munkáját és a cache feltöltését.

A mérés eredményét a provenance-szel együtt kell menteni, és a riportban külön kell jelölni, hogy a szám mért érték, becslés vagy célérték. A fejezet nem állít konkrét mért számot; a fenti táblázat a mérési kötelezettséget rögzíti, nem annak eredményét.

39.4.8 C/C++ összehasonlítás azonos workloadon

Az összehasonlítás csak akkor érvényes, ha a két implementáció ugyanazt a bemenetet ugyanazzal a feldolgozási szerződéssel dolgozza fel. Ellenőrizni kell, hogy a C változat és a C++ változat ugyanazt a corpus-t kapja, ugyanazt a kimenetet állítja elő, és a mérés ugyanazon a ponton történik. Gyakori hiba, hogy a C++ változat több másolást végez a kényelmesebb értékszemantika miatt, vagy a C változat a benchmarkban elkerüli a valós kliensútvonalat. Az összehasonlításnak ezért legalább három szinten kell futnia: allokációszám, latency és bináris méret. A throughput csak akkor értelmezhető, ha a párhuzamosság és a bufferelés azonos.

A mérési terv elfogadási feltételeit a csapat rögzíti. Például: a funkcionális egyenértékűség kötelező; az allokációszám nem nő a baseline-hoz képest; a p95 latency nem nő 10%-nál többet; a stripped bináris méret nem haladja meg a rögzített keretet. Ezek célértékek, nem mért eredmények, és a port akkor tekinthető késznek, ha a mért értékek ezeket teljesítik a dokumentált profilon.

39.4.9 Gyakori hibák és javításuk

Az egyik leggyakoribb hiba, hogy a warning kapu minden targetre érvényes, beleértve a harmadik féltől származó fejléceket is; ilyenkor a build a függőség miatt áll meg. Javítás: SYSTEM include vagy külön interfész target. A második, hogy az ASan és a TSan ugyanabban a buildben szerepel; ez nem támogatott, és félrevezető hibákat ad. Javítás: külön buildkönyvtár és külön CI-futás. A harmadik, hogy a golden teszt nem determinisztikus kimenetre hasonlít; javítás: normalizálás vagy a teszt átalakítása determinisztikus részhalmazra.

További tipikus hiba, hogy a fuzz target nem a valós parser belépési pontját hívja, hanem egy egyszerűsített másolatot; így a fuzz nem a szállított kódot vizsgálja. Az allokációs hiba tesztje gyakran csak azt nézi, hogy a folyamat nem omlik össze, de nem ellenőrzi a leaket és a dokumentált hibakódot. Az FFI smoke tesztet C++ fordítóval fordítják, amitől a C-kompatibilitás nem derül ki. A benchmarkot optimalizálatlan buildben futtatják, vagy a mérés a konstrukciót méri a feldolgozás helyett. Végül a provenance hiányzik, ezért a mérési eredmény később nem reprodukálható.

39.4.10 Feladatok

  1. Diagnosztikai feladat. Adott egy CMake-projekt, amelyben a -Werror minden targetre globálisan be van állítva, és egy harmadik féltől származó header figyelmeztetést okoz. Javítsd a buildet a warning kapu gyengítése nélkül.
  2. Kódolási feladat. Írj CTest-tesztet, amely egy 1 048 577 bájtos bemenetet (262 144 darab a=0\n rekord és egy további bájt) a CLI-vel elutasíttat, és ellenőrzi a dokumentált kilépési kódot és diagnosztikát. Magyarázd el, miért nem elég a WILL_FAIL.
  3. Kódolási feladat. Készíts allokációs hiba tesztet a C API-hoz, amely a dg_allocator callbackjével injektál hibát, és ellenőrzi, hogy a hiba a határon a dokumentált módon (NULL handle) jelenik meg, a felszabadító callback nem fut, és nincs leak.
  4. Mérési feladat. Tervezz mérési tervet a C és a C++ parser összehasonlítására. Add meg a workloadot, a metrikákat, az ismétlésszámot és azt, hogy milyen állítást nem lehet a mérésből levonni.

39.4.11 Megoldások

1. megoldás. A harmadik féltől származó fejlécet SYSTEM include-ként kell felvenni, például target_include_directories(diagcore_core SYSTEM PUBLIC ...), vagy a külső függőséget külön interfész targetbe kell tenni, amely nem linkeli a diag_warnings-t. Így a saját kód továbbra is -Werror alatt marad, a külső header pedig nem töri meg a buildet. Ellenőrzési feltétel: a saját forrásokban szándékosan bevezetett figyelmeztetés továbbra is hibát okoz, a külső header viszont nem.

2. megoldás. A WILL_FAIL csak azt vizsgálja, hogy a visszatérési kód nem nulla, ezért egy összeomlás vagy rossz hibakód is „sikeres” negatív teszt lenne. Helyette a tesztszkript maga ellenőrzi a kilépési kódot és a pontos diagnosztikát, a határ mindkét oldalán:

#!/bin/sh
# tests/reject_oversized.sh DIAG -- a limit utáni első bájt elutasítása
set -u
diag=$1
dir=$(mktemp -d) || exit 2
trap 'rm -rf "$dir"' EXIT

awk 'BEGIN { for (i = 0; i < 262144; i++) printf "a=0\n" }' \
    > "$dir/limit.in"
cp "$dir/limit.in" "$dir/over.in"
printf 'b' >> "$dir/over.in"

"$diag" "$dir/limit.in" > "$dir/ok.out" || exit 1
test "$(cat "$dir/ok.out")" = 'OK records=262144 sum=0 bytes=1048576' ||
    exit 1

rc=0
"$diag" "$dir/over.in" > "$dir/over.out" 2> "$dir/over.err" || rc=$?
test "$rc" -eq 4 || exit 1
test ! -s "$dir/over.out" || exit 1
test "$(cat "$dir/over.err")" = 'ERR code=limit offset=1048576'
add_test(NAME reject_oversized_input
  COMMAND sh tests/reject_oversized.sh $<TARGET_FILE:diagcore_cli>
  WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
set_tests_properties(reject_oversized_input PROPERTIES LABELS "negative")

A várt érték a projektszerződésből következik: pontosan 1 048 576 bájt még elfogadható, a következő bájt limit hibát ad, az offset 1 048 576, a kilépési kód 4, és a stdout üres. Ellenőrzési feltétel: a limitméretű bemenet sikeres, a plusz egy bájtos elutasítva, és a folyamat nem omlik össze. A szkriptet a C és a C++ CLI-vel is lefuttattuk (GCC 13.3.0), mindkettővel sikeresen; ellenpróbaként a cat paranccsal futtatva hibát jelzett.

3. megoldás. Az allokátor első hívása NULL-t ad és megszámolja a hívást; a felszabadító callback abort()-ot hív, mert sikertelen dg_create után nincs mit felszabadítani. A teszt ellenőrzi, hogy a dg_create NULL-t ad, és az allokátort pontosan egyszer hívta. A C++ wrapperben a placement new csak sikeres foglalás után fut, így félkész objektum sem keletkezik. A fenti tests/c_api_smoke.c második fele pontosan ez a teszt. Ellenőrzési feltétel: nincs kivétel a C határon, a felszabadító nem fut, és a LeakSanitizer nem jelez.

4. megoldás. A workload legyen ugyanaz a golden corpus és egy szintetikus stream, azonos párhuzamossággal. A metrikák: allokációszám, p95 latency, throughput és stripped bináris méret. Az ismétlésszám legalább 30, warmup után, külön folyamatban. A mérésből nem lehet azt kijelenteni, hogy „a C++ minden esetben gyorsabb” vagy hogy „a C++ memóriabiztonságosabb”; csak azt, hogy az adott profilon, adott workloadon milyen értékek adódtak. A funkcionális egyenértékűséget a golden tesztek igazolják, a teljesítményt a mérési harness; a kettőt nem szabad összekeverni.

39.4.12 Összefoglaló

A C++ záróprojekt build-, teszt- és mérési csomagja akkor elfogadható, ha a projektprofil és a provenance rögzíti a környezetet, a target-based CMake a saját kódra érvényesíti a warning kaput, a CTest kategóriákra bontja a teszteket, a golden corpus normalizált és determinisztikus, a fuzz és az allokációs hiba teszt a valós belépési pontot vizsgálja, az ASan/UBSan és a TSan külön fut, a C API-t C fordítóval is lefordítjuk, és a mérési terv előre rögzíti a metrikákat, a kontrollokat és az elfogadási feltételeket. A port funkcionális egyenértékűsége és a teljesítménye csak ezekkel a feltételekkel válik ellenőrizhető állítássá.