diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f933013c..39ece459 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,33 @@ jobs: path: .pio/build/default/firmware.bin if-no-files-found: error + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build + + - name: Cache googletest source + uses: actions/cache@v4 + with: + path: build/test/_deps/googletest-src + key: ${{ runner.os }}-googletest-${{ hashFiles('test/CMakeLists.txt') }} + + - name: Configure + run: cmake -S test -B build/test -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build/test + + - name: Run tests + run: ctest --test-dir build/test --output-on-failure -j + # This job is used as the PR required actions check, allows for changes to other steps in the future without breaking # PR requirements. test-status: @@ -110,6 +137,7 @@ jobs: - build - clang-format - cppcheck + - unit-tests if: always() runs-on: ubuntu-latest steps: diff --git a/platformio.ini b/platformio.ini index 213b5f49..80d98bd0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,6 +53,7 @@ extra_scripts = pre:scripts/gen_i18n.py pre:scripts/git_branch.py pre:scripts/patch_jpegdec.py + post:scripts/register_unit_tests_target.py ; Libraries lib_deps = diff --git a/scripts/register_unit_tests_target.py b/scripts/register_unit_tests_target.py new file mode 100644 index 00000000..e62bf76c --- /dev/null +++ b/scripts/register_unit_tests_target.py @@ -0,0 +1,28 @@ +""" +PlatformIO post-load script: register a `unit-tests` custom target so +`pio run -t unit-tests` builds and runs the host gtest suites under test/. + +The target shells out to CMake/CTest; the gtest framework is fetched and +the suites are built outside the PlatformIO/ESP-IDF toolchain (this is a +host build, not a firmware build). +""" + +import os + +Import("env") # noqa: F821 -- provided by PlatformIO at script load + +PROJECT_DIR = env["PROJECT_DIR"] # noqa: F821 +BUILD_DIR = os.path.join(PROJECT_DIR, "build", "test") +TEST_SRC_DIR = os.path.join(PROJECT_DIR, "test") + +env.AddCustomTarget( # noqa: F821 + name="unit-tests", + dependencies=None, + actions=[ + f'cmake -S "{TEST_SRC_DIR}" -B "{BUILD_DIR}" -DCMAKE_BUILD_TYPE=Release', + f'cmake --build "{BUILD_DIR}"', + f'ctest --test-dir "{BUILD_DIR}" --output-on-failure -j', + ], + title="Host unit tests", + description="Build and run gtest suites in test/ via CMake/CTest", +) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..31ce26bf --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.16) +project(crosspoint_reader_tests CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +include(FetchContent) + +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.17.0 +) + +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) +set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +set(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..") + +add_library(crosspoint_test_common INTERFACE) +target_include_directories(crosspoint_test_common INTERFACE + ${REPO_ROOT} + ${REPO_ROOT}/lib +) +target_compile_options(crosspoint_test_common INTERFACE + -Wall + -Wextra + -pedantic +) + +enable_testing() +include(GoogleTest) + +add_subdirectory(streaming_json_parser) +add_subdirectory(release_json_parser) +add_subdirectory(differential_rounding) +add_subdirectory(hyphenation_eval) diff --git a/test/README b/test/README index 9b1e87bc..2317a3aa 100644 --- a/test/README +++ b/test/README @@ -1,11 +1,15 @@ +Host-side gtest unit tests for crosspoint-reader. -This directory is intended for PlatformIO Test Runner and project tests. +Build and run: -Unit Testing is a software testing method by which individual units of -source code, sets of one or more MCU program modules together with associated -control data, usage procedures, and operating procedures, are tested to -determine whether they are fit for use. Unit testing finds problems early -in the development cycle. + cmake -S test -B build/test + cmake --build build/test + ctest --test-dir build/test --output-on-failure -j -More information about PlatformIO Unit Testing: -- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html +Run a single suite directly: + + cmake --build build/test --target StreamingJsonParserTest + build/test/streaming_json_parser/StreamingJsonParserTest --gtest_filter='*' + +Google Test is fetched via CMake FetchContent on first configure; the pinned +version lives in test/CMakeLists.txt. diff --git a/test/differential_rounding/CMakeLists.txt b/test/differential_rounding/CMakeLists.txt new file mode 100644 index 00000000..b958f6eb --- /dev/null +++ b/test/differential_rounding/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable(DifferentialRoundingTest + DifferentialRoundingTest.cpp + ${REPO_ROOT}/lib/EpdFont/EpdFont.cpp + ${REPO_ROOT}/lib/Utf8/Utf8.cpp +) + +target_include_directories(DifferentialRoundingTest PRIVATE + ${REPO_ROOT}/lib/EpdFont + ${REPO_ROOT}/lib/Utf8 +) + +target_link_libraries(DifferentialRoundingTest PRIVATE + crosspoint_test_common + GTest::gtest_main +) + +gtest_discover_tests(DifferentialRoundingTest) diff --git a/test/differential_rounding/DifferentialRoundingTest.cpp b/test/differential_rounding/DifferentialRoundingTest.cpp index fd454839..9efed6a8 100644 --- a/test/differential_rounding/DifferentialRoundingTest.cpp +++ b/test/differential_rounding/DifferentialRoundingTest.cpp @@ -1,34 +1,11 @@ -#include +#include + #include -#include #include #include "lib/EpdFont/EpdFont.h" #include "lib/EpdFont/EpdFontData.h" -static int testsPassed = 0; -static int testsFailed = 0; - -#define ASSERT_EQ(a, b) \ - do { \ - if ((a) != (b)) { \ - fprintf(stderr, " FAIL: %s:%d: %s == %d, expected %d\n", __FILE__, __LINE__, #a, (a), (b)); \ - testsFailed++; \ - return; \ - } \ - } while (0) - -#define ASSERT_TRUE(cond) \ - do { \ - if (!(cond)) { \ - fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ - testsFailed++; \ - return; \ - } \ - } while (0) - -#define PASS() testsPassed++ - // ============================================================================ // Synthetic test font // @@ -43,8 +20,10 @@ static int testsFailed = 0; // o->a: -2 (-0.125px) o->o: -3 (-0.1875px) // ============================================================================ +namespace { + // clang-format off -static const EpdGlyph kGlyphs[] = { +const EpdGlyph kGlyphs[] = { // idx width height advanceX left top dataLength dataOffset /* 0 'T' */ { 8, 12, 137, 0, 12, 0, 0 }, /* 1 'a' */ { 7, 8, 130, 0, 8, 0, 0 }, @@ -52,28 +31,28 @@ static const EpdGlyph kGlyphs[] = { /* 3 'x' */ { 7, 8, 136, 0, 8, 0, 0 }, }; -static const EpdUnicodeInterval kIntervals[] = { +const EpdUnicodeInterval kIntervals[] = { { 0x54, 0x54, 0 }, // 'T' -> glyph[0] { 0x61, 0x61, 1 }, // 'a' -> glyph[1] { 0x6F, 0x6F, 2 }, // 'o' -> glyph[2] { 0x78, 0x78, 3 }, // 'x' -> glyph[3] }; -static const EpdKernClassEntry kKernLeft[] = { +const EpdKernClassEntry kKernLeft[] = { { 0x54, 1 }, // 'T' -> left class 1 { 0x6F, 2 }, // 'o' -> left class 2 }; -static const EpdKernClassEntry kKernRight[] = { +const EpdKernClassEntry kKernRight[] = { { 0x61, 1 }, // 'a' -> right class 1 { 0x6F, 2 }, // 'o' -> right class 2 }; // Flat matrix: leftClassCount(2) x rightClassCount(2), 4.4 fixed-point // [L1,R1]=kern(T,a) [L1,R2]=kern(T,o) [L2,R1]=kern(o,a) [L2,R2]=kern(o,o) -static const int8_t kKernMatrix[] = { -5, -7, -2, -3 }; +const int8_t kKernMatrix[] = { -5, -7, -2, -3 }; -static const EpdFontData kTestFontData = { +const EpdFontData kTestFontData = { .bitmap = nullptr, .glyph = kGlyphs, .intervals = kIntervals, @@ -94,62 +73,63 @@ static const EpdFontData kTestFontData = { .kernRightClassCount = 2, .ligaturePairs = nullptr, .ligaturePairCount = 0, + .glyphMissHandler = nullptr, + .glyphMissCtx = nullptr, }; // clang-format on -static EpdFont testFont(&kTestFontData); +EpdFont& testFont() { + static EpdFont font(&kTestFontData); + return font; +} -// Helper: return width from getTextDimensions -static int textWidth(const char* str) { +int textWidth(const char* str) { int w = 0, h = 0; - testFont.getTextDimensions(str, &w, &h); + testFont().getTextDimensions(str, &w, &h); return w; } -static int textHeight(const char* str) { +int textHeight(const char* str) { int w = 0, h = 0; - testFont.getTextDimensions(str, &w, &h); + testFont().getTextDimensions(str, &w, &h); return h; } +// Simulate the old absolute-snap gap for comparison +int absoluteGap(int32_t startFP, int32_t advanceFP, int32_t kernFP) { + int32_t nextFP = startFP + advanceFP + kernFP; + return fp4::toPixel(nextFP) - fp4::toPixel(startFP); +} + +} // namespace + // ============================================================================ // Part 1: Pure fp4 math tests // ============================================================================ -// Simulate the old absolute-snap gap for comparison -static int absoluteGap(int32_t startFP, int32_t advanceFP, int32_t kernFP) { - int32_t nextFP = startFP + advanceFP + kernFP; - return fp4::toPixel(nextFP) - fp4::toPixel(startFP); -} - -void testFp4Basics() { - printf("testFp4Basics...\n"); - +TEST(Fp4Math, RoundTripIntegerPixels) { for (int px = 0; px < 500; px++) { - ASSERT_EQ(fp4::toPixel(fp4::fromPixel(px)), px); + EXPECT_EQ(fp4::toPixel(fp4::fromPixel(px)), px) << "px=" << px; } - - ASSERT_EQ(fp4::toPixel(0), 0); - ASSERT_EQ(fp4::toPixel(7), 0); // 0.4375 -> 0 - ASSERT_EQ(fp4::toPixel(8), 1); // 0.5 -> 1 (round half up) - ASSERT_EQ(fp4::toPixel(15), 1); // 0.9375 -> 1 - ASSERT_EQ(fp4::toPixel(16), 1); // 1.0 -> 1 - ASSERT_EQ(fp4::toPixel(24), 2); // 1.5 -> 2 - ASSERT_EQ(fp4::toPixel(-8), 0); // -0.5 -> 0 - ASSERT_EQ(fp4::toPixel(-9), -1); // -0.5625 -> -1 - ASSERT_EQ(fp4::toPixel(-16), -1); - - ASSERT_EQ(fp4::toPixel(137 + (-9)), 8); // 128 = 8.0 exact - ASSERT_EQ(fp4::toPixel(137 + (-5)), 8); // 132 = 8.25 - ASSERT_EQ(fp4::toPixel(137 + (-1)), 9); // 136 = 8.5 (half rounds up) - - printf(" All fp4 basics passed\n"); - PASS(); } -void testOldApproachInconsistency() { - printf("testOldApproachInconsistency...\n"); +TEST(Fp4Math, RoundingBoundaries) { + EXPECT_EQ(fp4::toPixel(0), 0); + EXPECT_EQ(fp4::toPixel(7), 0); // 0.4375 -> 0 + EXPECT_EQ(fp4::toPixel(8), 1); // 0.5 -> 1 (round half up) + EXPECT_EQ(fp4::toPixel(15), 1); // 0.9375 -> 1 + EXPECT_EQ(fp4::toPixel(16), 1); // 1.0 -> 1 + EXPECT_EQ(fp4::toPixel(24), 2); // 1.5 -> 2 + EXPECT_EQ(fp4::toPixel(-8), 0); // -0.5 -> 0 + EXPECT_EQ(fp4::toPixel(-9), -1); // -0.5625 -> -1 + EXPECT_EQ(fp4::toPixel(-16), -1); + EXPECT_EQ(fp4::toPixel(137 + (-9)), 8); // 128 = 8.0 exact + EXPECT_EQ(fp4::toPixel(137 + (-5)), 8); // 132 = 8.25 + EXPECT_EQ(fp4::toPixel(137 + (-1)), 9); // 136 = 8.5 (half rounds up) +} + +TEST(Fp4Math, OldApproachInconsistency) { // 'oo' pair: advance=145 (9.0625px), kern=-3 (-0.1875px), combined=142 (8.875px) const int32_t advance = 145; const int32_t kern = -3; @@ -164,76 +144,51 @@ void testOldApproachInconsistency() { } } - ASSERT_TRUE(maxGap - minGap >= 1); - printf(" Old absolute gap range: [%d, %d] -- varies by %d px\n", minGap, maxGap, maxGap - minGap); - - int diffStep = fp4::toPixel(advance + kern); - printf(" Differential step: always %d px\n", diffStep); - PASS(); + // Absolute snap produces inconsistent gaps depending on subpixel phase. + EXPECT_GE(maxGap - minGap, 1); } -void testExhaustiveKernRange() { - printf("testExhaustiveKernRange...\n"); - +TEST(Fp4Math, ExhaustiveKernRange) { const int32_t baseAdvance = 128; - int checked = 0; for (int advFrac = 0; advFrac < 16; advFrac++) { int32_t advance = baseAdvance + advFrac; for (int kern = -128; kern <= 127; kern++) { int step = fp4::toPixel(advance + static_cast(kern)); float idealPx = fp4::toFloat(advance + kern); - if (std::abs(step - idealPx) >= 1.0f) { - fprintf(stderr, " FAIL: advance=%d, kern=%d, step=%d, ideal=%.4f\n", advance, kern, step, idealPx); - testsFailed++; - return; - } - checked++; + EXPECT_LT(std::abs(step - idealPx), 1.0f) << "advance=" << advance << " kern=" << kern; } } - - printf(" Checked %d (advance, kern) combinations -- all within 1px of ideal\n", checked); - PASS(); } // ============================================================================ // Part 2: Integration tests using real EpdFont::getTextDimensions // ============================================================================ -void testKernLookup() { - printf("testKernLookup...\n"); - - ASSERT_EQ(testFont.getKerning('T', 'a'), -5); - ASSERT_EQ(testFont.getKerning('T', 'o'), -7); - ASSERT_EQ(testFont.getKerning('o', 'a'), -2); - ASSERT_EQ(testFont.getKerning('o', 'o'), -3); - ASSERT_EQ(testFont.getKerning('a', 'o'), 0); // 'a' has no left class - ASSERT_EQ(testFont.getKerning('x', 'o'), 0); // 'x' has no left class - ASSERT_EQ(testFont.getKerning('T', 'x'), 0); // 'x' has no right class - ASSERT_EQ(testFont.getKerning('T', 'T'), 0); // 'T' has no right class - - printf(" All kern lookups correct\n"); - PASS(); +TEST(EpdFont, KernLookup) { + EXPECT_EQ(testFont().getKerning('T', 'a'), -5); + EXPECT_EQ(testFont().getKerning('T', 'o'), -7); + EXPECT_EQ(testFont().getKerning('o', 'a'), -2); + EXPECT_EQ(testFont().getKerning('o', 'o'), -3); + EXPECT_EQ(testFont().getKerning('a', 'o'), 0); // 'a' has no left class + EXPECT_EQ(testFont().getKerning('x', 'o'), 0); // 'x' has no left class + EXPECT_EQ(testFont().getKerning('T', 'x'), 0); // 'x' has no right class + EXPECT_EQ(testFont().getKerning('T', 'T'), 0); // 'T' has no right class } -void testGlyphLookup() { - printf("testGlyphLookup...\n"); - - ASSERT_TRUE(testFont.getGlyph('T') != nullptr); - ASSERT_TRUE(testFont.getGlyph('a') != nullptr); - ASSERT_TRUE(testFont.getGlyph('o') != nullptr); - ASSERT_TRUE(testFont.getGlyph('x') != nullptr); - ASSERT_EQ(testFont.getGlyph('T')->advanceX, 137); - ASSERT_EQ(testFont.getGlyph('a')->advanceX, 130); - ASSERT_EQ(testFont.getGlyph('o')->advanceX, 145); - ASSERT_EQ(testFont.getGlyph('x')->advanceX, 136); +TEST(EpdFont, GlyphLookup) { + ASSERT_NE(testFont().getGlyph('T'), nullptr); + ASSERT_NE(testFont().getGlyph('a'), nullptr); + ASSERT_NE(testFont().getGlyph('o'), nullptr); + ASSERT_NE(testFont().getGlyph('x'), nullptr); + EXPECT_EQ(testFont().getGlyph('T')->advanceX, 137); + EXPECT_EQ(testFont().getGlyph('a')->advanceX, 130); + EXPECT_EQ(testFont().getGlyph('o')->advanceX, 145); + EXPECT_EQ(testFont().getGlyph('x')->advanceX, 136); // No U+FFFD in font, so unknown codepoints return nullptr - ASSERT_TRUE(testFont.getGlyph('Z') == nullptr); - ASSERT_TRUE(testFont.getGlyph('b') == nullptr); - - printf(" All glyph lookups correct\n"); - PASS(); + EXPECT_EQ(testFont().getGlyph('Z'), nullptr); + EXPECT_EQ(testFont().getGlyph('b'), nullptr); } // Known-value regression tests. Expected widths are computed by hand using @@ -245,45 +200,32 @@ void testGlyphLookup() { // // Differential step from glyph A to glyph B: // step = fp4::toPixel(advanceA + kern(A,B)) -void testKnownWidths() { - printf("testKnownWidths...\n"); - - // "o": single glyph at x=0, width=8 - // w = 0 + 8 = 8 - ASSERT_EQ(textWidth("o"), 8); +TEST(EpdFont, KnownWidths) { + // "o": single glyph at x=0, width=8 -> w = 0 + 8 = 8 + EXPECT_EQ(textWidth("o"), 8); // "oo": step = toPixel(145 + (-3)) = toPixel(142) = 9 // o1 at 0, o2 at 9. w = 9 + 8 = 17 - ASSERT_EQ(textWidth("oo"), 17); + EXPECT_EQ(textWidth("oo"), 17); - // "ooo": two steps of 9 - // o1 at 0, o2 at 9, o3 at 18. w = 18 + 8 = 26 - ASSERT_EQ(textWidth("ooo"), 26); + // "ooo": two steps of 9 -> o3 at 18, w = 18 + 8 = 26 + EXPECT_EQ(textWidth("ooo"), 26); - // "To": step = toPixel(137 + (-7)) = toPixel(130) = 8 - // T at 0, o at 8. w = 8 + 8 = 16 - ASSERT_EQ(textWidth("To"), 16); + // "To": step = toPixel(137 + (-7)) = 8 -> o at 8, w = 8 + 8 = 16 + EXPECT_EQ(textWidth("To"), 16); - // "Ta": step = toPixel(137 + (-5)) = toPixel(132) = 8 - // T at 0, a at 8. w = 8 + 7 = 15 - ASSERT_EQ(textWidth("Ta"), 15); + // "Ta": step = toPixel(137 + (-5)) = 8 -> a at 8, w = 8 + 7 = 15 + EXPECT_EQ(textWidth("Ta"), 15); - // "oa": step = toPixel(145 + (-2)) = toPixel(143) = 9 - // o at 0, a at 9. w = 9 + 7 = 16 - ASSERT_EQ(textWidth("oa"), 16); + // "oa": step = toPixel(145 + (-2)) = 9 -> a at 9, w = 9 + 7 = 16 + EXPECT_EQ(textWidth("oa"), 16); - // "Too": T at 0. - // step T->o = toPixel(137 + (-7)) = 8. o1 at 8. - // step o->o = toPixel(145 + (-3)) = 9. o2 at 17. - // w = 17 + 8 = 25 - ASSERT_EQ(textWidth("Too"), 25); + // "Too": T at 0, o1 at 8 (T->o step), o2 at 17 (o->o step). w = 17 + 8 = 25 + EXPECT_EQ(textWidth("Too"), 25); - // "xo": step = toPixel(136 + 0) = toPixel(136) = 9 (no kern: x has no left class) + // "xo": step = toPixel(136 + 0) = 9 (no kern: x has no left class) // x at 0, o at 9. w = 9 + 8 = 17 - ASSERT_EQ(textWidth("xo"), 17); - - printf(" All known widths correct\n"); - PASS(); + EXPECT_EQ(textWidth("xo"), 17); } // "oo" pair consistency: the pixel gap between two o's must be the same @@ -291,9 +233,7 @@ void testKnownWidths() { // differential rounding. With absolute snapping, "xoo" would produce a // different oo gap than "oo" because 'x' advance (136 FP) puts the first // 'o' at fractional phase 8, crossing the rounding boundary differently. -void testPairConsistencyViaFont() { - printf("testPairConsistencyViaFont...\n"); - +TEST(EpdFont, PairConsistencyViaFont) { // The oo gap = width(prefix + "oo") - width(prefix + "o") // This isolates the pixel distance contributed by the second 'o'. const int oo_gap_bare = textWidth("oo") - textWidth("o"); @@ -301,81 +241,33 @@ void testPairConsistencyViaFont() { const int oo_gap_after_T = textWidth("Too") - textWidth("To"); const int oo_gap_after_o = textWidth("ooo") - textWidth("oo"); - printf(" oo gap (bare): %d\n", oo_gap_bare); - printf(" oo gap (after x): %d\n", oo_gap_after_x); - printf(" oo gap (after T): %d\n", oo_gap_after_T); - printf(" oo gap (after o): %d\n", oo_gap_after_o); - - // All must be identical - ASSERT_EQ(oo_gap_after_x, oo_gap_bare); - ASSERT_EQ(oo_gap_after_T, oo_gap_bare); - ASSERT_EQ(oo_gap_after_o, oo_gap_bare); - - printf(" All oo gaps identical (%d px) regardless of prefix\n", oo_gap_bare); - PASS(); + EXPECT_EQ(oo_gap_after_x, oo_gap_bare); + EXPECT_EQ(oo_gap_after_T, oo_gap_bare); + EXPECT_EQ(oo_gap_after_o, oo_gap_bare); } // Null-glyph handling: when a codepoint has no glyph (and no replacement // glyph), the pending advance from the previous glyph must still be flushed. // Without the flush fix, the glyph after the null would overlap the one before. -void testNullGlyphAdvancePreserved() { - printf("testNullGlyphAdvancePreserved...\n"); - +TEST(EpdFont, NullGlyphAdvancePreserved) { // 'Z' (0x5A) is not in our font and there's no U+FFFD, so getGlyph returns null. // "oZo" should lay out as: o1 at 0, Z skipped (advance flushed), o2 at 9. // toPixel(145) = 9 (o's advance, no kern since Z resets prevCp). // w = 9 + 8 = 17 - int w = textWidth("oZo"); - printf(" width(\"oZo\") = %d\n", w); - - // Without the flush fix, o2 would land at 0 (overlapping o1), giving w = 8. - ASSERT_TRUE(w > 8); - ASSERT_EQ(w, 17); + EXPECT_EQ(textWidth("oZo"), 17); // Multi-null: "oZZo" -- two consecutive nulls, advance still preserved. - w = textWidth("oZZo"); - printf(" width(\"oZZo\") = %d\n", w); - ASSERT_EQ(w, 17); + EXPECT_EQ(textWidth("oZZo"), 17); // Null at start: "Zo" -- no pending advance to flush, o renders at 0. - w = textWidth("Zo"); - printf(" width(\"Zo\") = %d\n", w); - ASSERT_EQ(w, 8); - - printf(" Null-glyph advance correctly preserved\n"); - PASS(); + EXPECT_EQ(textWidth("Zo"), 8); } -void testHeightCalculation() { - printf("testHeightCalculation...\n"); - +TEST(EpdFont, HeightCalculation) { // 'T' is tallest: top=12, height=12 -> extent [0, 12) // 'o' and 'a': top=8, height=8 -> extent [0, 8) - ASSERT_EQ(textHeight("o"), 8); - ASSERT_EQ(textHeight("T"), 12); - ASSERT_EQ(textHeight("To"), 12); - ASSERT_EQ(textHeight("oo"), 8); - - printf(" All heights correct\n"); - PASS(); -} - -int main() { - printf("=== Differential Rounding Tests ===\n\n"); - - // Part 1: Pure fp4 math - testFp4Basics(); - testOldApproachInconsistency(); - testExhaustiveKernRange(); - - // Part 2: Integration tests against real EpdFont - testKernLookup(); - testGlyphLookup(); - testKnownWidths(); - testPairConsistencyViaFont(); - testNullGlyphAdvancePreserved(); - testHeightCalculation(); - - printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed); - return testsFailed > 0 ? 1 : 0; + EXPECT_EQ(textHeight("o"), 8); + EXPECT_EQ(textHeight("T"), 12); + EXPECT_EQ(textHeight("To"), 12); + EXPECT_EQ(textHeight("oo"), 8); } diff --git a/test/hyphenation_eval/CMakeLists.txt b/test/hyphenation_eval/CMakeLists.txt new file mode 100644 index 00000000..7cc7ba15 --- /dev/null +++ b/test/hyphenation_eval/CMakeLists.txt @@ -0,0 +1,24 @@ +add_executable(HyphenationEvaluationTest + HyphenationEvaluationTest.cpp + ${REPO_ROOT}/lib/Epub/Epub/hyphenation/Hyphenator.cpp + ${REPO_ROOT}/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp + ${REPO_ROOT}/lib/Epub/Epub/hyphenation/LiangHyphenation.cpp + ${REPO_ROOT}/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp + ${REPO_ROOT}/lib/Utf8/Utf8.cpp +) + +target_include_directories(HyphenationEvaluationTest PRIVATE + ${REPO_ROOT}/lib/Epub + ${REPO_ROOT}/lib/Utf8 +) + +target_compile_definitions(HyphenationEvaluationTest PRIVATE + HYPHENATION_RESOURCES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/resources" +) + +target_link_libraries(HyphenationEvaluationTest PRIVATE + crosspoint_test_common + GTest::gtest_main +) + +gtest_discover_tests(HyphenationEvaluationTest) diff --git a/test/hyphenation_eval/HyphenationEvaluationTest.cpp b/test/hyphenation_eval/HyphenationEvaluationTest.cpp index 0ac7eabc..45715ed8 100644 --- a/test/hyphenation_eval/HyphenationEvaluationTest.cpp +++ b/test/hyphenation_eval/HyphenationEvaluationTest.cpp @@ -1,10 +1,8 @@ #include +#include #include -#include -#include #include -#include #include #include #include @@ -14,6 +12,12 @@ #include "lib/Epub/Epub/hyphenation/LanguageHyphenator.h" #include "lib/Epub/Epub/hyphenation/LanguageRegistry.h" +#ifndef HYPHENATION_RESOURCES_DIR +#error "HYPHENATION_RESOURCES_DIR must be defined by the build system" +#endif + +namespace { + struct TestCase { std::string word; std::string hyphenated; @@ -31,23 +35,6 @@ struct EvaluationResult { double weightedScore = 0.0; }; -struct LanguageConfig { - std::string cliName; - std::string testDataFile; - const char* primaryTag; -}; - -const std::vector kSupportedLanguages = { - {"english", "test/hyphenation_eval/resources/english_hyphenation_tests.txt", "en"}, - {"french", "test/hyphenation_eval/resources/french_hyphenation_tests.txt", "fr"}, - {"german", "test/hyphenation_eval/resources/german_hyphenation_tests.txt", "de"}, - {"russian", "test/hyphenation_eval/resources/russian_hyphenation_tests.txt", "ru"}, - {"spanish", "test/hyphenation_eval/resources/spanish_hyphenation_tests.txt", "es"}, - {"italian", "test/hyphenation_eval/resources/italian_hyphenation_tests.txt", "it"}, - {"polish", "test/hyphenation_eval/resources/polish_hyphenation_tests.txt", "pl"}, - {"swedish", "test/hyphenation_eval/resources/swedish_hyphenation_tests.txt", "sv"}, -}; - std::vector expectedPositionsFromAnnotatedWord(const std::string& annotated) { std::vector positions; const unsigned char* ptr = reinterpret_cast(annotated.c_str()); @@ -72,7 +59,6 @@ std::vector loadTestData(const std::string& filename) { std::ifstream file(filename); if (!file.is_open()) { - std::cerr << "Error: Could not open file " << filename << std::endl; return testCases; } @@ -90,14 +76,11 @@ std::vector loadTestData(const std::string& filename) { testCase.word = word; testCase.hyphenated = hyphenated; testCase.frequency = std::stoi(freqStr); - testCase.expectedPositions = expectedPositionsFromAnnotatedWord(hyphenated); - testCases.push_back(testCase); } } - file.close(); return testCases; } @@ -133,30 +116,12 @@ std::string positionsToHyphenated(const std::string& word, const std::vector hyphenateWordWithHyphenator(const std::string& word, const LanguageHyphenator& hyphenator) { auto cps = collectCodepoints(word); trimSurroundingPunctuationAndFootnote(cps); - return hyphenator.breakIndexes(cps); } -std::vector resolveLanguages(const std::string& selection) { - if (selection == "all") { - return kSupportedLanguages; - } - - for (const auto& config : kSupportedLanguages) { - if (config.cliName == selection) { - return {config}; - } - } - - return {}; -} - -EvaluationResult evaluateWord(const TestCase& testCase, - std::function(const std::string&)> hyphenateFunc) { +EvaluationResult evaluateWord(const TestCase& testCase, const std::vector& actualPositions) { EvaluationResult result; - std::vector actualPositions = hyphenateFunc(testCase.word); - std::vector expected = testCase.expectedPositions; std::vector actual = actualPositions; @@ -189,8 +154,7 @@ EvaluationResult evaluateWord(const TestCase& testCase, result.f1Score = 2 * result.precision * result.recall / (result.precision + result.recall); } - // Treat words that contain no hyphenation marks in both the expected data and the - // algorithmic output as perfect matches so they don't drag down the per-word averages. + // Treat words with no expected and no actual hyphenation marks as perfect. if (expected.empty() && actual.empty()) { result.precision = 1.0; result.recall = 1.0; @@ -199,9 +163,8 @@ EvaluationResult evaluateWord(const TestCase& testCase, double fpPenalty = 2.0; double fnPenalty = 1.0; - int totalErrors = result.falsePositives * fpPenalty + result.falseNegatives * fnPenalty; - int totalPossible = expected.size() * fpPenalty; + int totalPossible = static_cast(expected.size() * fpPenalty); if (totalPossible > 0) { result.weightedScore = 1.0 - (static_cast(totalErrors) / totalPossible); @@ -213,180 +176,59 @@ EvaluationResult evaluateWord(const TestCase& testCase, return result; } -void printResults(const std::string& language, const std::vector& testCases, - const std::vector>& worstCases, int perfectMatches, - int partialMatches, int completeMisses, double totalPrecision, double totalRecall, double totalF1, - double totalWeighted, int totalTP, int totalFP, int totalFN, - std::function(const std::string&)> hyphenateFunc) { - std::string lang_upper = language; - if (!lang_upper.empty()) { - lang_upper[0] = std::toupper(lang_upper[0]); - } +// Runs the evaluation for a single language and asserts the per-word average F1 +// is at or above `minF1Percent`. Thresholds are set ~1pp below measured +// baselines so unrelated tweaks don't fail CI but real regressions still trip. +void runLanguageEval(const char* langName, const char* primaryTag, const char* resourceFile, double minF1Percent) { + const auto* hyphenator = getLanguageHyphenatorForPrimaryTag(primaryTag); + ASSERT_NE(hyphenator, nullptr) << "No hyphenator registered for tag: " << primaryTag; - std::cout << "================================================================================" << std::endl; - std::cout << lang_upper << " HYPHENATION EVALUATION RESULTS" << std::endl; - std::cout << "================================================================================" << std::endl; - std::cout << std::endl; + std::string path = std::string(HYPHENATION_RESOURCES_DIR) + "/" + resourceFile; + std::vector testCases = loadTestData(path); + ASSERT_FALSE(testCases.empty()) << "No test cases loaded from " << path; - std::cout << "Total test cases: " << testCases.size() << std::endl; - std::cout << "Perfect matches: " << perfectMatches << " (" << (perfectMatches * 100.0 / testCases.size()) << "%)" - << std::endl; - std::cout << "Partial matches: " << partialMatches << std::endl; - std::cout << "Complete misses: " << completeMisses << std::endl; - std::cout << std::endl; + double totalF1 = 0.0; + std::vector> imperfect; - std::cout << "--- Overall Metrics (averaged per word) ---" << std::endl; - std::cout << "Average Precision: " << (totalPrecision / testCases.size() * 100.0) << "%" << std::endl; - std::cout << "Average Recall: " << (totalRecall / testCases.size() * 100.0) << "%" << std::endl; - std::cout << "Average F1 Score: " << (totalF1 / testCases.size() * 100.0) << "%" << std::endl; - std::cout << "Average Weighted Score: " << (totalWeighted / testCases.size() * 100.0) << "% (FP penalty: 2x)" - << std::endl; - std::cout << std::endl; - - std::cout << "--- Overall Metrics (total counts) ---" << std::endl; - std::cout << "True Positives: " << totalTP << std::endl; - std::cout << "False Positives: " << totalFP << " (incorrect hyphenation points)" << std::endl; - std::cout << "False Negatives: " << totalFN << " (missed hyphenation points)" << std::endl; - - double overallPrecision = totalTP + totalFP > 0 ? static_cast(totalTP) / (totalTP + totalFP) : 0.0; - double overallRecall = totalTP + totalFN > 0 ? static_cast(totalTP) / (totalTP + totalFN) : 0.0; - double overallF1 = overallPrecision + overallRecall > 0 - ? 2 * overallPrecision * overallRecall / (overallPrecision + overallRecall) - : 0.0; - - std::cout << "Overall Precision: " << (overallPrecision * 100.0) << "%" << std::endl; - std::cout << "Overall Recall: " << (overallRecall * 100.0) << "%" << std::endl; - std::cout << "Overall F1 Score: " << (overallF1 * 100.0) << "%" << std::endl; - std::cout << std::endl; - - // Filter out perfect matches from the “worst cases” section so that only actionable failures appear. - auto hasImperfection = [](const EvaluationResult& r) { return r.weightedScore < 0.999999; }; - std::vector> imperfectCases; - imperfectCases.reserve(worstCases.size()); - for (const auto& entry : worstCases) { - if (hasImperfection(entry.second)) { - imperfectCases.push_back(entry); + for (const auto& tc : testCases) { + std::vector actual = hyphenateWordWithHyphenator(tc.word, *hyphenator); + EvaluationResult res = evaluateWord(tc, actual); + totalF1 += res.f1Score; + if (res.weightedScore < 0.999999) { + imperfect.emplace_back(tc, res); } } - std::cout << "--- Worst Cases (lowest weighted scores) ---" << std::endl; - int showCount = std::min(10, static_cast(imperfectCases.size())); - for (int i = 0; i < showCount; i++) { - const auto& testCase = imperfectCases[i].first; - const auto& result = imperfectCases[i].second; + double averageF1Percent = totalF1 / testCases.size() * 100.0; + ::testing::Test::RecordProperty("avg_f1_percent", std::to_string(averageF1Percent)); + ::testing::Test::RecordProperty("test_cases", std::to_string(testCases.size())); - std::vector actualPositions = hyphenateFunc(testCase.word); - std::string actualHyphenated = positionsToHyphenated(testCase.word, actualPositions); + std::cout << langName << ": F1=" << averageF1Percent << "% (threshold " << minF1Percent << "%, " << testCases.size() + << " cases)\n"; - std::cout << "Word: " << testCase.word << " (freq: " << testCase.frequency << ")" << std::endl; - std::cout << " Expected: " << testCase.hyphenated << std::endl; - std::cout << " Got: " << actualHyphenated << std::endl; - std::cout << " Precision: " << (result.precision * 100.0) << "%" - << " Recall: " << (result.recall * 100.0) << "%" - << " F1: " << (result.f1Score * 100.0) << "%" - << " Weighted: " << (result.weightedScore * 100.0) << "%" << std::endl; - std::cout << " TP: " << result.truePositives << " FP: " << result.falsePositives - << " FN: " << result.falseNegatives << std::endl; - std::cout << std::endl; - } - - // Additional compact list of the worst ~100 words to aid iteration - int compactCount = std::min(100, static_cast(imperfectCases.size())); - if (compactCount > 0) { - std::cout << "--- Compact Worst Cases (" << compactCount << ") ---" << std::endl; - for (int i = 0; i < compactCount; i++) { - const auto& testCase = imperfectCases[i].first; - std::vector actualPositions = hyphenateFunc(testCase.word); - std::string actualHyphenated = positionsToHyphenated(testCase.word, actualPositions); - std::cout << testCase.word << " | exp:" << testCase.hyphenated << " | got:" << actualHyphenated << std::endl; - } - std::cout << std::endl; - } -} - -int main(int argc, char* argv[]) { - const bool summaryMode = argc <= 1; - const std::string languageSelection = summaryMode ? "all" : argv[1]; - - std::vector languages = resolveLanguages(languageSelection); - if (languages.empty()) { - std::cerr << "Unknown language: " << languageSelection << std::endl; - return 1; - } - - for (const auto& lang : languages) { - const auto* hyphenator = getLanguageHyphenatorForPrimaryTag(lang.primaryTag); - if (!hyphenator) { - std::cerr << "No hyphenator registered for tag: " << lang.primaryTag << std::endl; - continue; - } - const auto hyphenateFunc = [hyphenator](const std::string& word) { - return hyphenateWordWithHyphenator(word, *hyphenator); - }; - - if (!summaryMode) { - std::cout << "Loading test data from: " << lang.testDataFile << std::endl; - } - std::vector testCases = loadTestData(lang.testDataFile); - - if (testCases.empty()) { - std::cerr << "No test cases loaded for " << lang.cliName << ". Skipping." << std::endl; - continue; - } - - if (!summaryMode) { - std::cout << "Loaded " << testCases.size() << " test cases for " << lang.cliName << std::endl; - std::cout << std::endl; - } - - int perfectMatches = 0; - int partialMatches = 0; - int completeMisses = 0; - - double totalPrecision = 0.0; - double totalRecall = 0.0; - double totalF1 = 0.0; - double totalWeighted = 0.0; - - int totalTP = 0, totalFP = 0, totalFN = 0; - - std::vector> worstCases; - - for (const auto& testCase : testCases) { - EvaluationResult result = evaluateWord(testCase, hyphenateFunc); - - totalTP += result.truePositives; - totalFP += result.falsePositives; - totalFN += result.falseNegatives; - - totalPrecision += result.precision; - totalRecall += result.recall; - totalF1 += result.f1Score; - totalWeighted += result.weightedScore; - - if (result.f1Score == 1.0) { - perfectMatches++; - } else if (result.f1Score > 0.0) { - partialMatches++; - } else { - completeMisses++; - } - - worstCases.push_back({testCase, result}); - } - - if (summaryMode) { - const double averageF1Percent = testCases.empty() ? 0.0 : (totalF1 / testCases.size() * 100.0); - std::cout << lang.cliName << ": " << averageF1Percent << "%" << std::endl; - continue; - } - - std::sort(worstCases.begin(), worstCases.end(), + if (averageF1Percent < minF1Percent) { + std::sort(imperfect.begin(), imperfect.end(), [](const auto& a, const auto& b) { return a.second.weightedScore < b.second.weightedScore; }); - - printResults(lang.cliName, testCases, worstCases, perfectMatches, partialMatches, completeMisses, totalPrecision, - totalRecall, totalF1, totalWeighted, totalTP, totalFP, totalFN, hyphenateFunc); + std::cout << "Worst cases for " << langName << ":\n"; + int show = std::min(10, static_cast(imperfect.size())); + for (int i = 0; i < show; ++i) { + const TestCase& tc = imperfect[i].first; + std::vector actual = hyphenateWordWithHyphenator(tc.word, *hyphenator); + std::cout << " " << tc.word << " | expected=" << tc.hyphenated + << " | got=" << positionsToHyphenated(tc.word, actual) << "\n"; + } } - return 0; + EXPECT_GE(averageF1Percent, minF1Percent) << "Hyphenation quality regressed for " << langName; } + +} // namespace + +TEST(HyphenationEval, English) { runLanguageEval("english", "en", "english_hyphenation_tests.txt", 98.10); } +TEST(HyphenationEval, French) { runLanguageEval("french", "fr", "french_hyphenation_tests.txt", 99.00); } +TEST(HyphenationEval, German) { runLanguageEval("german", "de", "german_hyphenation_tests.txt", 96.73); } +TEST(HyphenationEval, Russian) { runLanguageEval("russian", "ru", "russian_hyphenation_tests.txt", 96.22); } +TEST(HyphenationEval, Spanish) { runLanguageEval("spanish", "es", "spanish_hyphenation_tests.txt", 98.02); } +TEST(HyphenationEval, Italian) { runLanguageEval("italian", "it", "italian_hyphenation_tests.txt", 98.99); } +TEST(HyphenationEval, Polish) { runLanguageEval("polish", "pl", "polish_hyphenation_tests.txt", 98.92); } +TEST(HyphenationEval, Swedish) { runLanguageEval("swedish", "sv", "swedish_hyphenation_tests.txt", 94.01); } diff --git a/test/release_json_parser/CMakeLists.txt b/test/release_json_parser/CMakeLists.txt new file mode 100644 index 00000000..d9d45392 --- /dev/null +++ b/test/release_json_parser/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(ReleaseJsonParserTest + ReleaseJsonParserTest.cpp + ${REPO_ROOT}/lib/JsonParser/ReleaseJsonParser.cpp + ${REPO_ROOT}/lib/JsonParser/StreamingJsonParser.cpp +) + +target_include_directories(ReleaseJsonParserTest PRIVATE + ${REPO_ROOT}/lib/JsonParser +) + +target_link_libraries(ReleaseJsonParserTest PRIVATE + crosspoint_test_common + GTest::gtest_main +) + +gtest_discover_tests(ReleaseJsonParserTest) diff --git a/test/release_json_parser/ReleaseJsonParserTest.cpp b/test/release_json_parser/ReleaseJsonParserTest.cpp index 130bdfbd..ed85f045 100644 --- a/test/release_json_parser/ReleaseJsonParserTest.cpp +++ b/test/release_json_parser/ReleaseJsonParserTest.cpp @@ -1,51 +1,13 @@ -#include -#include +#include + #include #include #include "lib/JsonParser/ReleaseJsonParser.h" -static int testsPassed = 0; -static int testsFailed = 0; +namespace { -#define ASSERT_EQ(a, b) \ - do { \ - auto _a = (a); \ - auto _b = (b); \ - if (_a != _b) { \ - fprintf(stderr, " FAIL: %s:%d: %s != expected\n", __FILE__, __LINE__, #a); \ - testsFailed++; \ - return; \ - } \ - } while (0) - -#define ASSERT_STREQ(a, b) \ - do { \ - const char* _a = (a); \ - const char* _b = (b); \ - if (strcmp(_a, _b) != 0) { \ - fprintf(stderr, " FAIL: %s:%d: \"%s\" != \"%s\"\n", __FILE__, __LINE__, _a, _b); \ - testsFailed++; \ - return; \ - } \ - } while (0) - -#define ASSERT_TRUE(cond) \ - do { \ - if (!(cond)) { \ - fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ - testsFailed++; \ - return; \ - } \ - } while (0) - -#define PASS() testsPassed++ - -// ============================================================================ -// Realistic GitHub release JSON payloads -// ============================================================================ - -static const char* kRealisticPretty = R"({ +const char* kRealisticPretty = R"({ "url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345", "assets_url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/assets", "upload_url": "https://uploads.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/assets{?name,label}", @@ -147,11 +109,10 @@ static const char* kRealisticPretty = R"({ } })"; -static const char* kRealisticMinified = +const char* kRealisticMinified = R"({"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345","assets_url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/assets","id":12345,"author":{"login":"releasebot","id":99887766,"node_id":"MDQ6VXNlcjk5ODg3NzY2","type":"User","site_admin":false},"tag_name":"v2.4.1","target_commitish":"main","name":"CrossPoint Reader v2.4.1","draft":false,"prerelease":false,"assets":[{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100001","id":100001,"name":"crosspoint-reader-v2.4.1-source.zip","uploader":{"login":"releasebot","id":99887766},"content_type":"application/zip","state":"uploaded","size":2048576,"download_count":42,"browser_download_url":"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/crosspoint-reader-v2.4.1-source.zip"},{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100002","id":100002,"name":"firmware.bin","uploader":{"login":"releasebot","id":99887766},"content_type":"application/octet-stream","state":"uploaded","size":1572864,"download_count":187,"browser_download_url":"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin"},{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100003","id":100003,"name":"checksums.sha256","uploader":{"login":"releasebot","id":99887766},"content_type":"text/plain","state":"uploaded","size":192,"download_count":15,"browser_download_url":"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/checksums.sha256"}],"body":"## What's Changed\n\n* Fixed orientation crash","reactions":{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/reactions","total_count":5,"+1":3}})"; -// Helper: feed JSON in fixed-size chunks -static void feedChunked(ReleaseJsonParser& p, const char* json, size_t chunkSize) { +void feedChunked(ReleaseJsonParser& p, const char* json, size_t chunkSize) { size_t len = strlen(json); for (size_t off = 0; off < len; off += chunkSize) { size_t n = len - off < chunkSize ? len - off : chunkSize; @@ -159,65 +120,50 @@ static void feedChunked(ReleaseJsonParser& p, const char* json, size_t chunkSize } } -// ============================================================================ -// Tests -// ============================================================================ - -void testRealisticPrettyPrinted() { - printf("testRealisticPrettyPrinted...\n"); +} // namespace +TEST(ReleaseJsonParser, RealisticPrettyPrinted) { ReleaseJsonParser p; p.feed(kRealisticPretty, strlen(kRealisticPretty)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v2.4.1"); - ASSERT_STREQ(p.getFirmwareUrl(), + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v2.4.1"); + EXPECT_STREQ(p.getFirmwareUrl(), "https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin"); - ASSERT_EQ(p.getFirmwareSize(), 1572864u); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(p.getFirmwareSize(), 1572864u); } -void testRealisticMinified() { - printf("testRealisticMinified...\n"); - +TEST(ReleaseJsonParser, RealisticMinified) { ReleaseJsonParser p; p.feed(kRealisticMinified, strlen(kRealisticMinified)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v2.4.1"); - ASSERT_STREQ(p.getFirmwareUrl(), + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v2.4.1"); + EXPECT_STREQ(p.getFirmwareUrl(), "https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin"); - ASSERT_EQ(p.getFirmwareSize(), 1572864u); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(p.getFirmwareSize(), 1572864u); } -void testPrettyAndMinifiedAgree() { - printf("testPrettyAndMinifiedAgree...\n"); - +TEST(ReleaseJsonParser, PrettyAndMinifiedAgree) { ReleaseJsonParser pretty; pretty.feed(kRealisticPretty, strlen(kRealisticPretty)); ReleaseJsonParser minified; minified.feed(kRealisticMinified, strlen(kRealisticMinified)); - ASSERT_STREQ(pretty.getTagName(), minified.getTagName()); - ASSERT_STREQ(pretty.getFirmwareUrl(), minified.getFirmwareUrl()); - ASSERT_EQ(pretty.getFirmwareSize(), minified.getFirmwareSize()); + ASSERT_TRUE(pretty.foundTag()); + ASSERT_TRUE(pretty.foundFirmware()); + ASSERT_TRUE(minified.foundTag()); + ASSERT_TRUE(minified.foundFirmware()); - printf(" passed\n"); - PASS(); + EXPECT_STREQ(pretty.getTagName(), minified.getTagName()); + EXPECT_STREQ(pretty.getFirmwareUrl(), minified.getFirmwareUrl()); + EXPECT_EQ(pretty.getFirmwareSize(), minified.getFirmwareSize()); } -void testFirmwareNotFirstAsset() { - printf("testFirmwareNotFirstAsset...\n"); - - // firmware.bin is the third of four assets +TEST(ReleaseJsonParser, FirmwareNotFirstAsset) { const char* json = R"({ "tag_name": "v1.0.0", "assets": [ @@ -231,19 +177,14 @@ void testFirmwareNotFirstAsset() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v1.0.0"); - ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/firmware.bin"); - ASSERT_EQ(p.getFirmwareSize(), 987654u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v1.0.0"); + EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/firmware.bin"); + EXPECT_EQ(p.getFirmwareSize(), 987654u); } -void testFieldOrderUrlBeforeName() { - printf("testFieldOrderUrlBeforeName...\n"); - +TEST(ReleaseJsonParser, FieldOrderUrlBeforeName) { const char* json = R"({ "tag_name": "v3.0", "assets": [{ @@ -256,17 +197,12 @@ void testFieldOrderUrlBeforeName() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin"); - ASSERT_EQ(p.getFirmwareSize(), 2222u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin"); + EXPECT_EQ(p.getFirmwareSize(), 2222u); } -void testFieldOrderSizeBeforeUrl() { - printf("testFieldOrderSizeBeforeUrl...\n"); - +TEST(ReleaseJsonParser, FieldOrderSizeBeforeUrl) { const char* json = R"({ "tag_name": "v3.1", "assets": [{ @@ -279,17 +215,12 @@ void testFieldOrderSizeBeforeUrl() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw2.bin"); - ASSERT_EQ(p.getFirmwareSize(), 3333u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw2.bin"); + EXPECT_EQ(p.getFirmwareSize(), 3333u); } -void testFieldOrderNameFirst() { - printf("testFieldOrderNameFirst...\n"); - +TEST(ReleaseJsonParser, FieldOrderNameFirst) { const char* json = R"({ "tag_name": "v3.2", "assets": [{ @@ -302,17 +233,12 @@ void testFieldOrderNameFirst() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw3.bin"); - ASSERT_EQ(p.getFirmwareSize(), 4444u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw3.bin"); + EXPECT_EQ(p.getFirmwareSize(), 4444u); } -void testAssetsBeforeTagName() { - printf("testAssetsBeforeTagName...\n"); - +TEST(ReleaseJsonParser, AssetsBeforeTagName) { // tag_name appears after assets in the JSON const char* json = R"({ "name": "Release", @@ -327,69 +253,51 @@ void testAssetsBeforeTagName() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v4.0"); - ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin"); - ASSERT_EQ(p.getFirmwareSize(), 5555u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v4.0"); + EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin"); + EXPECT_EQ(p.getFirmwareSize(), 5555u); } -void testChunkedFeedingRealisticSmallChunks() { - printf("testChunkedFeedingRealisticSmallChunks...\n"); - - // Simulate HTTP chunked transfer with small chunks (64 bytes) +TEST(ReleaseJsonParser, ChunkedFeedingSmallChunks) { ReleaseJsonParser p; feedChunked(p, kRealisticPretty, 64); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v2.4.1"); - ASSERT_STREQ(p.getFirmwareUrl(), + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v2.4.1"); + EXPECT_STREQ(p.getFirmwareUrl(), "https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin"); - ASSERT_EQ(p.getFirmwareSize(), 1572864u); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(p.getFirmwareSize(), 1572864u); } -void testChunkedFeedingByteByByte() { - printf("testChunkedFeedingByteByByte...\n"); - +TEST(ReleaseJsonParser, ChunkedFeedingByteByByte) { ReleaseJsonParser p; feedChunked(p, kRealisticMinified, 1); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v2.4.1"); - ASSERT_EQ(p.getFirmwareSize(), 1572864u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v2.4.1"); + EXPECT_EQ(p.getFirmwareSize(), 1572864u); } -void testChunkedFeedingVariousChunkSizes() { - printf("testChunkedFeedingVariousChunkSizes...\n"); - - for (size_t chunkSize : {3, 7, 13, 31, 97, 128, 256, 512, 1024}) { +TEST(ReleaseJsonParser, ChunkedFeedingVariousChunkSizes) { + for (size_t chunkSize : {3u, 7u, 13u, 31u, 97u, 128u, 256u, 512u, 1024u}) { ReleaseJsonParser p; feedChunked(p, kRealisticPretty, chunkSize); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v2.4.1"); - ASSERT_EQ(p.getFirmwareSize(), 1572864u); + EXPECT_TRUE(p.foundTag()) << "chunkSize=" << chunkSize; + EXPECT_TRUE(p.foundFirmware()) << "chunkSize=" << chunkSize; + EXPECT_STREQ(p.getTagName(), "v2.4.1") << "chunkSize=" << chunkSize; + EXPECT_STREQ(p.getFirmwareUrl(), + "https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin") + << "chunkSize=" << chunkSize; + EXPECT_EQ(p.getFirmwareSize(), 1572864u) << "chunkSize=" << chunkSize; } - - printf(" passed (9 chunk sizes)\n"); - PASS(); } -void testMissingTagName() { - printf("testMissingTagName...\n"); - +TEST(ReleaseJsonParser, MissingTagName) { const char* json = R"({ "name": "Some Release", "draft": false, @@ -403,17 +311,12 @@ void testMissingTagName() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(!p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), ""); - - printf(" passed\n"); - PASS(); + EXPECT_FALSE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), ""); } -void testMissingFirmwareBinAsset() { - printf("testMissingFirmwareBinAsset...\n"); - +TEST(ReleaseJsonParser, MissingFirmwareBinAsset) { const char* json = R"({ "tag_name": "v1.0.0", "assets": [ @@ -425,129 +328,88 @@ void testMissingFirmwareBinAsset() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(!p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v1.0.0"); - ASSERT_STREQ(p.getFirmwareUrl(), ""); - ASSERT_EQ(p.getFirmwareSize(), 0u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_FALSE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v1.0.0"); + EXPECT_STREQ(p.getFirmwareUrl(), ""); + EXPECT_EQ(p.getFirmwareSize(), 0u); } -void testEmptyAssetsArray() { - printf("testEmptyAssetsArray...\n"); - +TEST(ReleaseJsonParser, EmptyAssetsArray) { const char* json = R"({"tag_name": "v1.0.0", "assets": []})"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(!p.foundFirmware()); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_FALSE(p.foundFirmware()); } -void testNoAssetsKey() { - printf("testNoAssetsKey...\n"); - +TEST(ReleaseJsonParser, NoAssetsKey) { const char* json = R"({"tag_name": "v1.0.0", "name": "Release"})"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(!p.foundFirmware()); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_FALSE(p.foundFirmware()); } -void testTruncatedBeforeTagValue() { - printf("testTruncatedBeforeTagValue...\n"); - +TEST(ReleaseJsonParser, TruncatedBeforeTagValue) { const char* json = R"({"tag_name": )"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(!p.foundTag()); - ASSERT_TRUE(!p.foundFirmware()); - - printf(" passed\n"); - PASS(); + EXPECT_FALSE(p.foundTag()); + EXPECT_FALSE(p.foundFirmware()); } -void testTruncatedInsideTagValue() { - printf("testTruncatedInsideTagValue...\n"); - +TEST(ReleaseJsonParser, TruncatedInsideTagValue) { const char* json = R"({"tag_name": "v2.4)"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(!p.foundTag()); - - printf(" passed\n"); - PASS(); + EXPECT_FALSE(p.foundTag()); } -void testTruncatedInsideAssetsArray() { - printf("testTruncatedInsideAssetsArray...\n"); - +TEST(ReleaseJsonParser, TruncatedInsideAssetsArray) { const char* json = R"({"tag_name": "v2.4.1", "assets": [{"name": "firm)"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_STREQ(p.getTagName(), "v2.4.1"); - ASSERT_TRUE(!p.foundFirmware()); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_STREQ(p.getTagName(), "v2.4.1"); + EXPECT_FALSE(p.foundFirmware()); } -void testTruncatedAfterFirmwareName() { - printf("testTruncatedAfterFirmwareName...\n"); - +TEST(ReleaseJsonParser, TruncatedAfterFirmwareName) { // Found the name but connection dropped before URL/size const char* json = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_dow)"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(!p.foundFirmware()); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_FALSE(p.foundFirmware()); } -void testTruncatedRealisticJson() { - printf("testTruncatedRealisticJson...\n"); - - // Truncate the realistic JSON at various points; none should crash +TEST(ReleaseJsonParser, TruncatedRealisticJson) { std::string full(kRealisticPretty); for (size_t cutPoint : {10u, 50u, 100u, 200u, 500u, 1000u, 1500u, 2000u}) { if (cutPoint >= full.size()) continue; ReleaseJsonParser p; p.feed(full.c_str(), cutPoint); - // Just verify no crash; results depend on where we cut (void)p.foundTag(); (void)p.foundFirmware(); } - - printf(" passed (no crashes on truncated realistic JSON)\n"); - PASS(); + SUCCEED(); } -void testNestedObjectsInAsset() { - printf("testNestedObjectsInAsset...\n"); - +TEST(ReleaseJsonParser, NestedObjectsInAsset) { // Asset with deeply nested "uploader" object -- should not confuse depth tracking const char* json = R"({ "tag_name": "v5.0", @@ -566,17 +428,12 @@ void testNestedObjectsInAsset() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw5.bin"); - ASSERT_EQ(p.getFirmwareSize(), 8888u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw5.bin"); + EXPECT_EQ(p.getFirmwareSize(), 8888u); } -void testNestedObjectsAtTopLevel() { - printf("testNestedObjectsAtTopLevel...\n"); - +TEST(ReleaseJsonParser, NestedObjectsAtTopLevel) { // Multiple nested objects at the top level before/after tag_name and assets const char* json = R"({ "author": {"login": "dev", "id": 1, "nested": {"deep": true}}, @@ -589,18 +446,13 @@ void testNestedObjectsAtTopLevel() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v6.0"); - ASSERT_EQ(p.getFirmwareSize(), 1111u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v6.0"); + EXPECT_EQ(p.getFirmwareSize(), 1111u); } -void testArraysAtTopLevel() { - printf("testArraysAtTopLevel...\n"); - +TEST(ReleaseJsonParser, ArraysAtTopLevel) { // A non-assets array at the top level should not interfere const char* json = R"({ "tag_name": "v7.0", @@ -611,69 +463,53 @@ void testArraysAtTopLevel() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v7.0"); - ASSERT_EQ(p.getFirmwareSize(), 7070u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v7.0"); + EXPECT_EQ(p.getFirmwareSize(), 7070u); } -void testResetAndReuse() { - printf("testResetAndReuse...\n"); - +TEST(ReleaseJsonParser, ResetAndReuse) { ReleaseJsonParser p; const char* json1 = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":1}]})"; p.feed(json1, strlen(json1)); - ASSERT_TRUE(p.foundTag()); - ASSERT_STREQ(p.getTagName(), "v1.0"); - ASSERT_STREQ(p.getFirmwareUrl(), "https://a"); - ASSERT_EQ(p.getFirmwareSize(), 1u); + EXPECT_TRUE(p.foundTag()); + EXPECT_STREQ(p.getTagName(), "v1.0"); + EXPECT_STREQ(p.getFirmwareUrl(), "https://a"); + EXPECT_EQ(p.getFirmwareSize(), 1u); p.reset(); - // Second document with different values const char* json2 = R"({"tag_name":"v2.0","assets":[{"name":"firmware.bin","browser_download_url":"https://b","size":2}]})"; p.feed(json2, strlen(json2)); - ASSERT_TRUE(p.foundTag()); - ASSERT_STREQ(p.getTagName(), "v2.0"); - ASSERT_STREQ(p.getFirmwareUrl(), "https://b"); - ASSERT_EQ(p.getFirmwareSize(), 2u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_STREQ(p.getTagName(), "v2.0"); + EXPECT_STREQ(p.getFirmwareUrl(), "https://b"); + EXPECT_EQ(p.getFirmwareSize(), 2u); } -void testResetClearsState() { - printf("testResetClearsState...\n"); - +TEST(ReleaseJsonParser, ResetClearsState) { ReleaseJsonParser p; const char* json = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":100}]})"; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); p.reset(); - ASSERT_TRUE(!p.foundTag()); - ASSERT_TRUE(!p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), ""); - ASSERT_STREQ(p.getFirmwareUrl(), ""); - ASSERT_EQ(p.getFirmwareSize(), 0u); - - printf(" passed\n"); - PASS(); + EXPECT_FALSE(p.foundTag()); + EXPECT_FALSE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), ""); + EXPECT_STREQ(p.getFirmwareUrl(), ""); + EXPECT_EQ(p.getFirmwareSize(), 0u); } -void testPartialAssetNameMatch() { - printf("testPartialAssetNameMatch...\n"); - +TEST(ReleaseJsonParser, PartialAssetNameMatch) { // "firmware.bin.bak" should NOT match "firmware.bin" const char* json = R"({ "tag_name": "v1.0", @@ -686,16 +522,11 @@ void testPartialAssetNameMatch() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(!p.foundFirmware()); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_FALSE(p.foundFirmware()); } -void testFirmwareBinExactMatch() { - printf("testFirmwareBinExactMatch...\n"); - +TEST(ReleaseJsonParser, FirmwareBinExactMatch) { // Only exact "firmware.bin" matches, not similar names const char* json = R"({ "tag_name": "v1.0", @@ -709,17 +540,12 @@ void testFirmwareBinExactMatch() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getFirmwareUrl(), "https://exact"); - ASSERT_EQ(p.getFirmwareSize(), 200u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getFirmwareUrl(), "https://exact"); + EXPECT_EQ(p.getFirmwareSize(), 200u); } -void testLargeSize() { - printf("testLargeSize...\n"); - +TEST(ReleaseJsonParser, LargeSize) { // 16MB firmware (maximum flash size) const char* json = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":16777216}]})"; @@ -727,49 +553,34 @@ void testLargeSize() { ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_EQ(p.getFirmwareSize(), 16777216u); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(p.getFirmwareSize(), 16777216u); } -void testSizeZero() { - printf("testSizeZero...\n"); - +TEST(ReleaseJsonParser, SizeZero) { const char* json = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":0}]})"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_EQ(p.getFirmwareSize(), 0u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_EQ(p.getFirmwareSize(), 0u); } -void testMinimalValidJson() { - printf("testMinimalValidJson...\n"); - +TEST(ReleaseJsonParser, MinimalValidJson) { const char* json = R"({"tag_name":"v0","assets":[{"name":"firmware.bin","browser_download_url":"u","size":1}]})"; ReleaseJsonParser p; p.feed(json, strlen(json)); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v0"); - ASSERT_STREQ(p.getFirmwareUrl(), "u"); - ASSERT_EQ(p.getFirmwareSize(), 1u); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(p.foundTag()); + EXPECT_TRUE(p.foundFirmware()); + EXPECT_STREQ(p.getTagName(), "v0"); + EXPECT_STREQ(p.getFirmwareUrl(), "u"); + EXPECT_EQ(p.getFirmwareSize(), 1u); } -void testChunkedRealisticEveryBoundary() { - printf("testChunkedRealisticEveryBoundary...\n"); - +TEST(ReleaseJsonParser, ChunkedRealisticEveryBoundary) { // Two-chunk split at every byte boundary on a compact JSON const char* json = R"({"tag_name":"v2.0","assets":[{"name":"firmware.bin","browser_download_url":"https://example.com/fw","size":9999}]})"; @@ -780,54 +591,10 @@ void testChunkedRealisticEveryBoundary() { if (split > 0) p.feed(json, split); if (split < len) p.feed(json + split, len - split); - ASSERT_TRUE(p.foundTag()); - ASSERT_TRUE(p.foundFirmware()); - ASSERT_STREQ(p.getTagName(), "v2.0"); - ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw"); - ASSERT_EQ(p.getFirmwareSize(), 9999u); + EXPECT_TRUE(p.foundTag()) << "split=" << split; + EXPECT_TRUE(p.foundFirmware()) << "split=" << split; + EXPECT_STREQ(p.getTagName(), "v2.0") << "split=" << split; + EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw") << "split=" << split; + EXPECT_EQ(p.getFirmwareSize(), 9999u) << "split=" << split; } - - printf(" passed (all %zu split points)\n", len + 1); - PASS(); -} - -// ============================================================================ - -int main() { - printf("=== ReleaseJsonParser Tests ===\n\n"); - - testRealisticPrettyPrinted(); - testRealisticMinified(); - testPrettyAndMinifiedAgree(); - testFirmwareNotFirstAsset(); - testFieldOrderUrlBeforeName(); - testFieldOrderSizeBeforeUrl(); - testFieldOrderNameFirst(); - testAssetsBeforeTagName(); - testChunkedFeedingRealisticSmallChunks(); - testChunkedFeedingByteByByte(); - testChunkedFeedingVariousChunkSizes(); - testMissingTagName(); - testMissingFirmwareBinAsset(); - testEmptyAssetsArray(); - testNoAssetsKey(); - testTruncatedBeforeTagValue(); - testTruncatedInsideTagValue(); - testTruncatedInsideAssetsArray(); - testTruncatedAfterFirmwareName(); - testTruncatedRealisticJson(); - testNestedObjectsInAsset(); - testNestedObjectsAtTopLevel(); - testArraysAtTopLevel(); - testResetAndReuse(); - testResetClearsState(); - testPartialAssetNameMatch(); - testFirmwareBinExactMatch(); - testLargeSize(); - testSizeZero(); - testMinimalValidJson(); - testChunkedRealisticEveryBoundary(); - - printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed); - return testsFailed > 0 ? 1 : 0; } diff --git a/test/run_differential_rounding_test.sh b/test/run_differential_rounding_test.sh deleted file mode 100755 index dd7aa5c7..00000000 --- a/test/run_differential_rounding_test.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BUILD_DIR="$ROOT_DIR/build/differential_rounding" -BINARY="$BUILD_DIR/DifferentialRoundingTest" - -mkdir -p "$BUILD_DIR" - -SOURCES=( - "$ROOT_DIR/test/differential_rounding/DifferentialRoundingTest.cpp" - "$ROOT_DIR/lib/EpdFont/EpdFont.cpp" - "$ROOT_DIR/lib/Utf8/Utf8.cpp" -) - -CXXFLAGS=( - -std=c++20 - -O2 - -Wall - -Wextra - -pedantic - -I"$ROOT_DIR" - -I"$ROOT_DIR/lib" - -I"$ROOT_DIR/lib/EpdFont" - -I"$ROOT_DIR/lib/Utf8" -) - -c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY" - -"$BINARY" "$@" diff --git a/test/run_hyphenation_eval.sh b/test/run_hyphenation_eval.sh deleted file mode 100755 index 0d4bba3d..00000000 --- a/test/run_hyphenation_eval.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BUILD_DIR="$ROOT_DIR/build/hyphenation_eval" -BINARY="$BUILD_DIR/HyphenationEvaluationTest" - -mkdir -p "$BUILD_DIR" - -SOURCES=( - "$ROOT_DIR/test/hyphenation_eval/HyphenationEvaluationTest.cpp" - "$ROOT_DIR/lib/Epub/Epub/hyphenation/Hyphenator.cpp" - "$ROOT_DIR/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp" - "$ROOT_DIR/lib/Epub/Epub/hyphenation/LiangHyphenation.cpp" - "$ROOT_DIR/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp" - "$ROOT_DIR/lib/Utf8/Utf8.cpp" -) - -CXXFLAGS=( - -std=c++20 - -O2 - -Wall - -Wextra - -pedantic - -I"$ROOT_DIR" - -I"$ROOT_DIR/lib" - -I"$ROOT_DIR/lib/Utf8" -) - -c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY" - -"$BINARY" "$@" diff --git a/test/run_release_json_parser_test.sh b/test/run_release_json_parser_test.sh deleted file mode 100755 index 1b212445..00000000 --- a/test/run_release_json_parser_test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BUILD_DIR="$ROOT_DIR/build/release_json_parser" -BINARY="$BUILD_DIR/ReleaseJsonParserTest" - -mkdir -p "$BUILD_DIR" - -SOURCES=( - "$ROOT_DIR/test/release_json_parser/ReleaseJsonParserTest.cpp" - "$ROOT_DIR/lib/JsonParser/ReleaseJsonParser.cpp" - "$ROOT_DIR/lib/JsonParser/StreamingJsonParser.cpp" -) - -CXXFLAGS=( - -std=c++20 - -O2 - -Wall - -Wextra - -pedantic - -I"$ROOT_DIR" - -I"$ROOT_DIR/lib" - -I"$ROOT_DIR/lib/JsonParser" -) - -c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY" - -"$BINARY" "$@" diff --git a/test/run_streaming_json_parser_test.sh b/test/run_streaming_json_parser_test.sh deleted file mode 100755 index 90e051c4..00000000 --- a/test/run_streaming_json_parser_test.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BUILD_DIR="$ROOT_DIR/build/streaming_json_parser" -BINARY="$BUILD_DIR/StreamingJsonParserTest" - -mkdir -p "$BUILD_DIR" - -SOURCES=( - "$ROOT_DIR/test/streaming_json_parser/StreamingJsonParserTest.cpp" - "$ROOT_DIR/lib/JsonParser/StreamingJsonParser.cpp" -) - -CXXFLAGS=( - -std=c++20 - -O2 - -Wall - -Wextra - -pedantic - -I"$ROOT_DIR" - -I"$ROOT_DIR/lib" - -I"$ROOT_DIR/lib/JsonParser" -) - -c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY" - -"$BINARY" "$@" diff --git a/test/streaming_json_parser/CMakeLists.txt b/test/streaming_json_parser/CMakeLists.txt new file mode 100644 index 00000000..7675ad3e --- /dev/null +++ b/test/streaming_json_parser/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(StreamingJsonParserTest + StreamingJsonParserTest.cpp + ${REPO_ROOT}/lib/JsonParser/StreamingJsonParser.cpp +) + +target_include_directories(StreamingJsonParserTest PRIVATE + ${REPO_ROOT}/lib/JsonParser +) + +target_link_libraries(StreamingJsonParserTest PRIVATE + crosspoint_test_common + GTest::gtest_main +) + +gtest_discover_tests(StreamingJsonParserTest) diff --git a/test/streaming_json_parser/StreamingJsonParserTest.cpp b/test/streaming_json_parser/StreamingJsonParserTest.cpp index 55b3a12a..bf983295 100644 --- a/test/streaming_json_parser/StreamingJsonParserTest.cpp +++ b/test/streaming_json_parser/StreamingJsonParserTest.cpp @@ -1,37 +1,13 @@ -#include -#include +#include + #include #include #include #include "lib/JsonParser/StreamingJsonParser.h" -static int testsPassed = 0; -static int testsFailed = 0; +namespace { -#define ASSERT_EQ(a, b) \ - do { \ - auto _a = (a); \ - auto _b = (b); \ - if (_a != _b) { \ - fprintf(stderr, " FAIL: %s:%d: %s != expected\n", __FILE__, __LINE__, #a); \ - testsFailed++; \ - return; \ - } \ - } while (0) - -#define ASSERT_TRUE(cond) \ - do { \ - if (!(cond)) { \ - fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ - testsFailed++; \ - return; \ - } \ - } while (0) - -#define PASS() testsPassed++ - -// Event types for recording callback sequences enum class EventType { KEY, STRING, @@ -54,40 +30,36 @@ struct TestContext { std::vector events; }; -static void onKey(void* ctx, const char* key, size_t len) { +void onKey(void* ctx, const char* key, size_t len) { static_cast(ctx)->events.push_back({EventType::KEY, std::string(key, len)}); } -static void onString(void* ctx, const char* value, size_t len) { +void onString(void* ctx, const char* value, size_t len) { static_cast(ctx)->events.push_back({EventType::STRING, std::string(value, len)}); } -static void onNumber(void* ctx, const char* value, size_t len) { +void onNumber(void* ctx, const char* value, size_t len) { static_cast(ctx)->events.push_back({EventType::NUMBER, std::string(value, len)}); } -static void onBool(void* ctx, bool value) { +void onBool(void* ctx, bool value) { static_cast(ctx)->events.push_back({value ? EventType::BOOL_TRUE : EventType::BOOL_FALSE, {}}); } -static void onNull(void* ctx) { static_cast(ctx)->events.push_back({EventType::NULL_VAL, {}}); } -static void onObjectStart(void* ctx) { - static_cast(ctx)->events.push_back({EventType::OBJECT_START, {}}); -} -static void onObjectEnd(void* ctx) { static_cast(ctx)->events.push_back({EventType::OBJECT_END, {}}); } -static void onArrayStart(void* ctx) { static_cast(ctx)->events.push_back({EventType::ARRAY_START, {}}); } -static void onArrayEnd(void* ctx) { static_cast(ctx)->events.push_back({EventType::ARRAY_END, {}}); } +void onNull(void* ctx) { static_cast(ctx)->events.push_back({EventType::NULL_VAL, {}}); } +void onObjectStart(void* ctx) { static_cast(ctx)->events.push_back({EventType::OBJECT_START, {}}); } +void onObjectEnd(void* ctx) { static_cast(ctx)->events.push_back({EventType::OBJECT_END, {}}); } +void onArrayStart(void* ctx) { static_cast(ctx)->events.push_back({EventType::ARRAY_START, {}}); } +void onArrayEnd(void* ctx) { static_cast(ctx)->events.push_back({EventType::ARRAY_END, {}}); } -static JsonCallbacks makeCallbacks(TestContext* ctx) { +JsonCallbacks makeCallbacks(TestContext* ctx) { return {ctx, onKey, onString, onNumber, onBool, onNull, onObjectStart, onObjectEnd, onArrayStart, onArrayEnd}; } -// Feed entire input at once -static std::vector parse(const char* json) { +std::vector parse(const char* json) { TestContext ctx; StreamingJsonParser parser(makeCallbacks(&ctx)); parser.feed(json, strlen(json)); return ctx.events; } -// Feed input one byte at a time -static std::vector parseBytewise(const char* json) { +std::vector parseBytewise(const char* json) { TestContext ctx; StreamingJsonParser parser(makeCallbacks(&ctx)); size_t len = strlen(json); @@ -97,176 +69,132 @@ static std::vector parseBytewise(const char* json) { return ctx.events; } -// ============================================================================ -// Tests -// ============================================================================ - -void testSimpleObject() { - printf("testSimpleObject...\n"); +} // namespace +TEST(StreamingJsonParser, SimpleObject) { auto events = parse(R"({"key": "value", "num": 42})"); ASSERT_EQ(events.size(), 6u); - ASSERT_EQ(events[0].type, EventType::OBJECT_START); - ASSERT_EQ(events[1].type, EventType::KEY); - ASSERT_EQ(events[1].value, "key"); - ASSERT_EQ(events[2].type, EventType::STRING); - ASSERT_EQ(events[2].value, "value"); - ASSERT_EQ(events[3].type, EventType::KEY); - ASSERT_EQ(events[3].value, "num"); - ASSERT_EQ(events[4].type, EventType::NUMBER); - ASSERT_EQ(events[4].value, "42"); - ASSERT_EQ(events[5].type, EventType::OBJECT_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::OBJECT_START); + EXPECT_EQ(events[1].type, EventType::KEY); + EXPECT_EQ(events[1].value, "key"); + EXPECT_EQ(events[2].type, EventType::STRING); + EXPECT_EQ(events[2].value, "value"); + EXPECT_EQ(events[3].type, EventType::KEY); + EXPECT_EQ(events[3].value, "num"); + EXPECT_EQ(events[4].type, EventType::NUMBER); + EXPECT_EQ(events[4].value, "42"); + EXPECT_EQ(events[5].type, EventType::OBJECT_END); } -void testNestedObjects() { - printf("testNestedObjects...\n"); - +TEST(StreamingJsonParser, NestedObjects) { auto events = parse(R"({"a": {"b": "c"}})"); ASSERT_EQ(events.size(), 7u); - ASSERT_EQ(events[0].type, EventType::OBJECT_START); - ASSERT_EQ(events[1].type, EventType::KEY); - ASSERT_EQ(events[1].value, "a"); - ASSERT_EQ(events[2].type, EventType::OBJECT_START); - ASSERT_EQ(events[3].type, EventType::KEY); - ASSERT_EQ(events[3].value, "b"); - ASSERT_EQ(events[4].type, EventType::STRING); - ASSERT_EQ(events[4].value, "c"); - ASSERT_EQ(events[5].type, EventType::OBJECT_END); - ASSERT_EQ(events[6].type, EventType::OBJECT_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::OBJECT_START); + EXPECT_EQ(events[1].type, EventType::KEY); + EXPECT_EQ(events[1].value, "a"); + EXPECT_EQ(events[2].type, EventType::OBJECT_START); + EXPECT_EQ(events[3].type, EventType::KEY); + EXPECT_EQ(events[3].value, "b"); + EXPECT_EQ(events[4].type, EventType::STRING); + EXPECT_EQ(events[4].value, "c"); + EXPECT_EQ(events[5].type, EventType::OBJECT_END); + EXPECT_EQ(events[6].type, EventType::OBJECT_END); } -void testArrayOfValues() { - printf("testArrayOfValues...\n"); - +TEST(StreamingJsonParser, ArrayOfValues) { auto events = parse(R"({"items": [1, "two", true, false, null]})"); ASSERT_EQ(events.size(), 10u); - ASSERT_EQ(events[0].type, EventType::OBJECT_START); - ASSERT_EQ(events[1].type, EventType::KEY); - ASSERT_EQ(events[1].value, "items"); - ASSERT_EQ(events[2].type, EventType::ARRAY_START); - ASSERT_EQ(events[3].type, EventType::NUMBER); - ASSERT_EQ(events[3].value, "1"); - ASSERT_EQ(events[4].type, EventType::STRING); - ASSERT_EQ(events[4].value, "two"); - ASSERT_EQ(events[5].type, EventType::BOOL_TRUE); - ASSERT_EQ(events[6].type, EventType::BOOL_FALSE); - ASSERT_EQ(events[7].type, EventType::NULL_VAL); - ASSERT_EQ(events[8].type, EventType::ARRAY_END); - ASSERT_EQ(events[9].type, EventType::OBJECT_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::OBJECT_START); + EXPECT_EQ(events[1].type, EventType::KEY); + EXPECT_EQ(events[1].value, "items"); + EXPECT_EQ(events[2].type, EventType::ARRAY_START); + EXPECT_EQ(events[3].type, EventType::NUMBER); + EXPECT_EQ(events[3].value, "1"); + EXPECT_EQ(events[4].type, EventType::STRING); + EXPECT_EQ(events[4].value, "two"); + EXPECT_EQ(events[5].type, EventType::BOOL_TRUE); + EXPECT_EQ(events[6].type, EventType::BOOL_FALSE); + EXPECT_EQ(events[7].type, EventType::NULL_VAL); + EXPECT_EQ(events[8].type, EventType::ARRAY_END); + EXPECT_EQ(events[9].type, EventType::OBJECT_END); } -void testArrayOfObjects() { - printf("testArrayOfObjects...\n"); - +TEST(StreamingJsonParser, ArrayOfObjects) { auto events = parse(R"([{"a": 1}, {"b": 2}])"); ASSERT_EQ(events.size(), 10u); - ASSERT_EQ(events[0].type, EventType::ARRAY_START); - ASSERT_EQ(events[1].type, EventType::OBJECT_START); - ASSERT_EQ(events[2].type, EventType::KEY); - ASSERT_EQ(events[2].value, "a"); - ASSERT_EQ(events[3].type, EventType::NUMBER); - ASSERT_EQ(events[3].value, "1"); - ASSERT_EQ(events[4].type, EventType::OBJECT_END); - ASSERT_EQ(events[5].type, EventType::OBJECT_START); - ASSERT_EQ(events[6].type, EventType::KEY); - ASSERT_EQ(events[6].value, "b"); - ASSERT_EQ(events[7].type, EventType::NUMBER); - ASSERT_EQ(events[7].value, "2"); - ASSERT_EQ(events[8].type, EventType::OBJECT_END); - ASSERT_EQ(events[9].type, EventType::ARRAY_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::ARRAY_START); + EXPECT_EQ(events[1].type, EventType::OBJECT_START); + EXPECT_EQ(events[2].type, EventType::KEY); + EXPECT_EQ(events[2].value, "a"); + EXPECT_EQ(events[3].type, EventType::NUMBER); + EXPECT_EQ(events[3].value, "1"); + EXPECT_EQ(events[4].type, EventType::OBJECT_END); + EXPECT_EQ(events[5].type, EventType::OBJECT_START); + EXPECT_EQ(events[6].type, EventType::KEY); + EXPECT_EQ(events[6].value, "b"); + EXPECT_EQ(events[7].type, EventType::NUMBER); + EXPECT_EQ(events[7].value, "2"); + EXPECT_EQ(events[8].type, EventType::OBJECT_END); + EXPECT_EQ(events[9].type, EventType::ARRAY_END); } -void testStringEscapes() { - printf("testStringEscapes...\n"); - +TEST(StreamingJsonParser, StringEscapes) { auto events = parse(R"({"esc": "a\"b\\c\/d\ne\tf"})"); ASSERT_EQ(events.size(), 4u); - ASSERT_EQ(events[2].type, EventType::STRING); - ASSERT_EQ(events[2].value, std::string("a\"b\\c/d\ne\tf")); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[2].type, EventType::STRING); + EXPECT_EQ(events[2].value, std::string("a\"b\\c/d\ne\tf")); } -void testUnicodeEscapePassthrough() { - printf("testUnicodeEscapePassthrough...\n"); - +TEST(StreamingJsonParser, UnicodeEscapePassthrough) { auto events = parse(R"({"u": "\u0041\u0042"})"); - ASSERT_EQ(events[2].type, EventType::STRING); + ASSERT_EQ(events.size(), 4u); + EXPECT_EQ(events[2].type, EventType::STRING); // \uXXXX passed through as literal \u followed by the hex digits - ASSERT_EQ(events[2].value, "\\u0041\\u0042"); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[2].value, "\\u0041\\u0042"); } -void testNumbers() { - printf("testNumbers...\n"); - +TEST(StreamingJsonParser, Numbers) { auto events = parse(R"({"int": 42, "neg": -7, "flt": 3.14, "exp": 1e10, "nexp": -2.5E-3})"); - ASSERT_EQ(events[2].type, EventType::NUMBER); - ASSERT_EQ(events[2].value, "42"); - ASSERT_EQ(events[4].type, EventType::NUMBER); - ASSERT_EQ(events[4].value, "-7"); - ASSERT_EQ(events[6].type, EventType::NUMBER); - ASSERT_EQ(events[6].value, "3.14"); - ASSERT_EQ(events[8].type, EventType::NUMBER); - ASSERT_EQ(events[8].value, "1e10"); - ASSERT_EQ(events[10].type, EventType::NUMBER); - ASSERT_EQ(events[10].value, "-2.5E-3"); - - printf(" passed\n"); - PASS(); + ASSERT_EQ(events.size(), 12u); + EXPECT_EQ(events[2].type, EventType::NUMBER); + EXPECT_EQ(events[2].value, "42"); + EXPECT_EQ(events[4].type, EventType::NUMBER); + EXPECT_EQ(events[4].value, "-7"); + EXPECT_EQ(events[6].type, EventType::NUMBER); + EXPECT_EQ(events[6].value, "3.14"); + EXPECT_EQ(events[8].type, EventType::NUMBER); + EXPECT_EQ(events[8].value, "1e10"); + EXPECT_EQ(events[10].type, EventType::NUMBER); + EXPECT_EQ(events[10].value, "-2.5E-3"); } -void testBooleansAndNull() { - printf("testBooleansAndNull...\n"); - +TEST(StreamingJsonParser, BooleansAndNull) { auto events = parse(R"({"t": true, "f": false, "n": null})"); - ASSERT_EQ(events[2].type, EventType::BOOL_TRUE); - ASSERT_EQ(events[4].type, EventType::BOOL_FALSE); - ASSERT_EQ(events[6].type, EventType::NULL_VAL); - - printf(" passed\n"); - PASS(); + ASSERT_EQ(events.size(), 8u); + EXPECT_EQ(events[2].type, EventType::BOOL_TRUE); + EXPECT_EQ(events[4].type, EventType::BOOL_FALSE); + EXPECT_EQ(events[6].type, EventType::NULL_VAL); } -void testChunkedFeeding() { - printf("testChunkedFeeding...\n"); - +TEST(StreamingJsonParser, ChunkedFeeding) { const char* json = R"({"key": "value", "num": 42, "arr": [1, 2]})"; auto reference = parse(json); - // Feed byte-by-byte and verify identical event sequence auto bytewise = parseBytewise(json); - ASSERT_EQ(bytewise.size(), reference.size()); for (size_t i = 0; i < reference.size(); ++i) { - ASSERT_EQ(bytewise[i].type, reference[i].type); - ASSERT_EQ(bytewise[i].value, reference[i].value); + EXPECT_EQ(bytewise[i].type, reference[i].type); + EXPECT_EQ(bytewise[i].value, reference[i].value); } - // Feed in chunks of varying size for (size_t chunkSize = 2; chunkSize <= 7; ++chunkSize) { TestContext ctx; StreamingJsonParser parser(makeCallbacks(&ctx)); @@ -277,20 +205,15 @@ void testChunkedFeeding() { parser.feed(json + offset, feedLen); } - ASSERT_EQ(ctx.events.size(), reference.size()); + ASSERT_EQ(ctx.events.size(), reference.size()) << "chunkSize=" << chunkSize; for (size_t i = 0; i < reference.size(); ++i) { - ASSERT_EQ(ctx.events[i].type, reference[i].type); - ASSERT_EQ(ctx.events[i].value, reference[i].value); + EXPECT_EQ(ctx.events[i].type, reference[i].type) << "chunkSize=" << chunkSize << " event=" << i; + EXPECT_EQ(ctx.events[i].value, reference[i].value) << "chunkSize=" << chunkSize << " event=" << i; } } - - printf(" passed (byte-by-byte + chunk sizes 2-7)\n"); - PASS(); } -void testEveryByteBoundary() { - printf("testEveryByteBoundary...\n"); - +TEST(StreamingJsonParser, EveryByteBoundary) { const char* json = R"({"tag_name":"v1.2.3","assets":[{"name":"firmware.bin","size":12345}]})"; auto reference = parse(json); size_t len = strlen(json); @@ -301,137 +224,94 @@ void testEveryByteBoundary() { if (split > 0) parser.feed(json, split); if (split < len) parser.feed(json + split, len - split); - ASSERT_EQ(ctx.events.size(), reference.size()); + ASSERT_EQ(ctx.events.size(), reference.size()) << "split=" << split; for (size_t i = 0; i < reference.size(); ++i) { - if (ctx.events[i].type != reference[i].type || ctx.events[i].value != reference[i].value) { - fprintf(stderr, " FAIL at split=%zu, event %zu\n", split, i); - testsFailed++; - return; - } + EXPECT_EQ(ctx.events[i].type, reference[i].type) << "split=" << split << " event=" << i; + EXPECT_EQ(ctx.events[i].value, reference[i].value) << "split=" << split << " event=" << i; } } - - printf(" passed (all %zu split points)\n", len + 1); - PASS(); } -void testLargeTokenTruncation() { - printf("testLargeTokenTruncation...\n"); - +TEST(StreamingJsonParser, LargeTokenTruncation) { // Build a string value that exceeds TOKEN_BUF_SIZE std::string longVal(StreamingJsonParser::TOKEN_BUF_SIZE + 100, 'x'); std::string json = R"({"short": "ok", "long": ")" + longVal + R"("})"; auto events = parse(json.c_str()); - // "short" key + "ok" value should still fire - ASSERT_TRUE(events.size() >= 3); - ASSERT_EQ(events[1].type, EventType::KEY); - ASSERT_EQ(events[1].value, "short"); - ASSERT_EQ(events[2].type, EventType::STRING); - ASSERT_EQ(events[2].value, "ok"); + ASSERT_GE(events.size(), 3u); + EXPECT_EQ(events[1].type, EventType::KEY); + EXPECT_EQ(events[1].value, "short"); + EXPECT_EQ(events[2].type, EventType::STRING); + EXPECT_EQ(events[2].value, "ok"); - // The "long" key fires, but the oversized value is silently dropped bool foundLongKey = false; bool foundLongValue = false; for (auto& e : events) { if (e.type == EventType::KEY && e.value == "long") foundLongKey = true; if (e.type == EventType::STRING && e.value.size() > 500) foundLongValue = true; } - ASSERT_TRUE(foundLongKey); - ASSERT_TRUE(!foundLongValue); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(foundLongKey); + EXPECT_FALSE(foundLongValue); } -void testEmptyObject() { - printf("testEmptyObject...\n"); - +TEST(StreamingJsonParser, EmptyObject) { auto events = parse("{}"); ASSERT_EQ(events.size(), 2u); - ASSERT_EQ(events[0].type, EventType::OBJECT_START); - ASSERT_EQ(events[1].type, EventType::OBJECT_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::OBJECT_START); + EXPECT_EQ(events[1].type, EventType::OBJECT_END); } -void testEmptyArray() { - printf("testEmptyArray...\n"); - +TEST(StreamingJsonParser, EmptyArray) { auto events = parse("[]"); ASSERT_EQ(events.size(), 2u); - ASSERT_EQ(events[0].type, EventType::ARRAY_START); - ASSERT_EQ(events[1].type, EventType::ARRAY_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::ARRAY_START); + EXPECT_EQ(events[1].type, EventType::ARRAY_END); } -void testNestedArrays() { - printf("testNestedArrays...\n"); - +TEST(StreamingJsonParser, NestedArrays) { auto events = parse("[[1, 2], [3]]"); ASSERT_EQ(events.size(), 9u); - ASSERT_EQ(events[0].type, EventType::ARRAY_START); - ASSERT_EQ(events[1].type, EventType::ARRAY_START); - ASSERT_EQ(events[2].type, EventType::NUMBER); - ASSERT_EQ(events[2].value, "1"); - ASSERT_EQ(events[3].type, EventType::NUMBER); - ASSERT_EQ(events[3].value, "2"); - ASSERT_EQ(events[4].type, EventType::ARRAY_END); - ASSERT_EQ(events[5].type, EventType::ARRAY_START); - ASSERT_EQ(events[6].type, EventType::NUMBER); - ASSERT_EQ(events[6].value, "3"); - ASSERT_EQ(events[7].type, EventType::ARRAY_END); - ASSERT_EQ(events[8].type, EventType::ARRAY_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::ARRAY_START); + EXPECT_EQ(events[1].type, EventType::ARRAY_START); + EXPECT_EQ(events[2].type, EventType::NUMBER); + EXPECT_EQ(events[2].value, "1"); + EXPECT_EQ(events[3].type, EventType::NUMBER); + EXPECT_EQ(events[3].value, "2"); + EXPECT_EQ(events[4].type, EventType::ARRAY_END); + EXPECT_EQ(events[5].type, EventType::ARRAY_START); + EXPECT_EQ(events[6].type, EventType::NUMBER); + EXPECT_EQ(events[6].value, "3"); + EXPECT_EQ(events[7].type, EventType::ARRAY_END); + EXPECT_EQ(events[8].type, EventType::ARRAY_END); } -void testTopLevelArray() { - printf("testTopLevelArray...\n"); - +TEST(StreamingJsonParser, TopLevelArray) { auto events = parse(R"(["hello", 42, true, null])"); ASSERT_EQ(events.size(), 6u); - ASSERT_EQ(events[0].type, EventType::ARRAY_START); - ASSERT_EQ(events[1].type, EventType::STRING); - ASSERT_EQ(events[1].value, "hello"); - ASSERT_EQ(events[2].type, EventType::NUMBER); - ASSERT_EQ(events[2].value, "42"); - ASSERT_EQ(events[3].type, EventType::BOOL_TRUE); - ASSERT_EQ(events[4].type, EventType::NULL_VAL); - ASSERT_EQ(events[5].type, EventType::ARRAY_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::ARRAY_START); + EXPECT_EQ(events[1].type, EventType::STRING); + EXPECT_EQ(events[1].value, "hello"); + EXPECT_EQ(events[2].type, EventType::NUMBER); + EXPECT_EQ(events[2].value, "42"); + EXPECT_EQ(events[3].type, EventType::BOOL_TRUE); + EXPECT_EQ(events[4].type, EventType::NULL_VAL); + EXPECT_EQ(events[5].type, EventType::ARRAY_END); } -void testWhitespaceVariants() { - printf("testWhitespaceVariants...\n"); - - // Minified +TEST(StreamingJsonParser, WhitespaceVariants) { auto minified = parse(R"({"a":1,"b":"x"})"); - - // Pretty-printed const char* pretty = "{\n \"a\": 1,\n \"b\": \"x\"\n}"; auto prettyEvents = parse(pretty); ASSERT_EQ(minified.size(), prettyEvents.size()); for (size_t i = 0; i < minified.size(); ++i) { - ASSERT_EQ(minified[i].type, prettyEvents[i].type); - ASSERT_EQ(minified[i].value, prettyEvents[i].value); + EXPECT_EQ(minified[i].type, prettyEvents[i].type); + EXPECT_EQ(minified[i].value, prettyEvents[i].value); } - - printf(" passed\n"); - PASS(); } -void testResetBetweenDocuments() { - printf("testResetBetweenDocuments...\n"); - +TEST(StreamingJsonParser, ResetBetweenDocuments) { TestContext ctx; StreamingJsonParser parser(makeCallbacks(&ctx)); @@ -445,52 +325,34 @@ void testResetBetweenDocuments() { const char* json2 = R"({"b": 2})"; parser.feed(json2, strlen(json2)); ASSERT_EQ(ctx.events.size(), 4u); - ASSERT_EQ(ctx.events[1].value, "b"); - ASSERT_EQ(ctx.events[2].value, "2"); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(ctx.events[1].value, "b"); + EXPECT_EQ(ctx.events[2].value, "2"); } -void testNumberAtEndOfInput() { - printf("testNumberAtEndOfInput...\n"); - - // Number terminated by end of input (no trailing whitespace or structural char). - // The parser must emit the number when feed() ends (after a closing brace). +TEST(StreamingJsonParser, NumberAtEndOfInput) { auto events = parse(R"({"n": 99})"); bool found = false; for (auto& e : events) { if (e.type == EventType::NUMBER && e.value == "99") found = true; } - ASSERT_TRUE(found); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(found); } -void testArrayOfStrings() { - printf("testArrayOfStrings...\n"); - +TEST(StreamingJsonParser, ArrayOfStrings) { auto events = parse(R"(["a", "b", "c"])"); ASSERT_EQ(events.size(), 5u); - ASSERT_EQ(events[0].type, EventType::ARRAY_START); - ASSERT_EQ(events[1].type, EventType::STRING); - ASSERT_EQ(events[1].value, "a"); - ASSERT_EQ(events[2].type, EventType::STRING); - ASSERT_EQ(events[2].value, "b"); - ASSERT_EQ(events[3].type, EventType::STRING); - ASSERT_EQ(events[3].value, "c"); - ASSERT_EQ(events[4].type, EventType::ARRAY_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::ARRAY_START); + EXPECT_EQ(events[1].type, EventType::STRING); + EXPECT_EQ(events[1].value, "a"); + EXPECT_EQ(events[2].type, EventType::STRING); + EXPECT_EQ(events[2].value, "b"); + EXPECT_EQ(events[3].type, EventType::STRING); + EXPECT_EQ(events[3].value, "c"); + EXPECT_EQ(events[4].type, EventType::ARRAY_END); } -void testTruncatedInputNoCrash() { - printf("testTruncatedInputNoCrash...\n"); - - // Simulates a connection drop mid-JSON. Parser must not crash. +TEST(StreamingJsonParser, TruncatedInputNoCrash) { const char* truncated[] = { R"({"key": "val)", R"({"key": )", R"({"key)", R"([1, 2, )", R"({"a": tru)", R"({"a": fal)", R"({"a": nul)", R"({"a": "hello\)", @@ -502,49 +364,36 @@ void testTruncatedInputNoCrash() { parser.feed(json, strlen(json)); // Just verify no crash; partial results are acceptable } - - printf(" passed (no crashes on %d truncated inputs)\n", 8); - PASS(); + SUCCEED(); } -void testAllEscapeSequences() { - printf("testAllEscapeSequences...\n"); - +TEST(StreamingJsonParser, AllEscapeSequences) { auto events = parse(R"({"e": "\b\f\n\r\t\"\\\/"})"); - ASSERT_EQ(events[2].type, EventType::STRING); - ASSERT_EQ(events[2].value, std::string("\b\f\n\r\t\"\\/")); - - printf(" passed\n"); - PASS(); + ASSERT_EQ(events.size(), 4u); + EXPECT_EQ(events[2].type, EventType::STRING); + EXPECT_EQ(events[2].value, std::string("\b\f\n\r\t\"\\/")); } -void testObjectInArray() { - printf("testObjectInArray...\n"); - +TEST(StreamingJsonParser, ObjectInArray) { // After an object closes inside an array, the next string after comma // should be correctly identified as a key (inside the next object) or // a string value (if directly in the array). auto events = parse(R"([{"k":"v"}, "bare"])"); ASSERT_EQ(events.size(), 7u); - ASSERT_EQ(events[0].type, EventType::ARRAY_START); - ASSERT_EQ(events[1].type, EventType::OBJECT_START); - ASSERT_EQ(events[2].type, EventType::KEY); - ASSERT_EQ(events[2].value, "k"); - ASSERT_EQ(events[3].type, EventType::STRING); - ASSERT_EQ(events[3].value, "v"); - ASSERT_EQ(events[4].type, EventType::OBJECT_END); - ASSERT_EQ(events[5].type, EventType::STRING); - ASSERT_EQ(events[5].value, "bare"); - ASSERT_EQ(events[6].type, EventType::ARRAY_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::ARRAY_START); + EXPECT_EQ(events[1].type, EventType::OBJECT_START); + EXPECT_EQ(events[2].type, EventType::KEY); + EXPECT_EQ(events[2].value, "k"); + EXPECT_EQ(events[3].type, EventType::STRING); + EXPECT_EQ(events[3].value, "v"); + EXPECT_EQ(events[4].type, EventType::OBJECT_END); + EXPECT_EQ(events[5].type, EventType::STRING); + EXPECT_EQ(events[5].value, "bare"); + EXPECT_EQ(events[6].type, EventType::ARRAY_END); } -void testDeeplyNested() { - printf("testDeeplyNested...\n"); - +TEST(StreamingJsonParser, DeeplyNested) { // 20 levels of nesting (well within MAX_NESTING=32) std::string json; for (int i = 0; i < 20; ++i) json += R"({"d":)"; @@ -555,18 +404,13 @@ void testDeeplyNested() { // 20 OBJECT_START + 20 KEY + 1 NUMBER + 20 OBJECT_END = 61 ASSERT_EQ(events.size(), 61u); - ASSERT_EQ(events[0].type, EventType::OBJECT_START); - ASSERT_EQ(events[40].type, EventType::NUMBER); - ASSERT_EQ(events[40].value, "0"); - ASSERT_EQ(events[60].type, EventType::OBJECT_END); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[0].type, EventType::OBJECT_START); + EXPECT_EQ(events[40].type, EventType::NUMBER); + EXPECT_EQ(events[40].value, "0"); + EXPECT_EQ(events[60].type, EventType::OBJECT_END); } -void testNestingOverflow() { - printf("testNestingOverflow...\n"); - +TEST(StreamingJsonParser, NestingOverflow) { // Exceed MAX_NESTING -- parser should set error flag, not crash std::string json; for (size_t i = 0; i < StreamingJsonParser::MAX_NESTING + 5; ++i) json += "["; @@ -575,47 +419,32 @@ void testNestingOverflow() { StreamingJsonParser parser(makeCallbacks(&ctx)); parser.feed(json.c_str(), json.size()); - ASSERT_TRUE(parser.hasError()); - - printf(" passed\n"); - PASS(); + EXPECT_TRUE(parser.hasError()); } -void testNumberZero() { - printf("testNumberZero...\n"); - +TEST(StreamingJsonParser, NumberZero) { auto events = parse(R"({"z": 0})"); - ASSERT_EQ(events[2].type, EventType::NUMBER); - ASSERT_EQ(events[2].value, "0"); - - printf(" passed\n"); - PASS(); + ASSERT_EQ(events.size(), 4u); + EXPECT_EQ(events[2].type, EventType::NUMBER); + EXPECT_EQ(events[2].value, "0"); } -void testMultipleValuesInObject() { - printf("testMultipleValuesInObject...\n"); - +TEST(StreamingJsonParser, MultipleValuesInObject) { auto events = parse(R"({"a": "x", "b": "y", "c": "z"})"); ASSERT_EQ(events.size(), 8u); - ASSERT_EQ(events[1].value, "a"); - ASSERT_EQ(events[2].value, "x"); - ASSERT_EQ(events[3].value, "b"); - ASSERT_EQ(events[4].value, "y"); - ASSERT_EQ(events[5].value, "c"); - ASSERT_EQ(events[6].value, "z"); - - printf(" passed\n"); - PASS(); + EXPECT_EQ(events[1].value, "a"); + EXPECT_EQ(events[2].value, "x"); + EXPECT_EQ(events[3].value, "b"); + EXPECT_EQ(events[4].value, "y"); + EXPECT_EQ(events[5].value, "c"); + EXPECT_EQ(events[6].value, "z"); } -void testChunkedSplitInsideString() { - printf("testChunkedSplitInsideString...\n"); - +TEST(StreamingJsonParser, ChunkedSplitInsideString) { const char* json = R"({"key": "hello world"})"; auto reference = parse(json); - // Split right in the middle of "hello world" size_t splitAt = 14; // inside the string value TestContext ctx; StreamingJsonParser parser(makeCallbacks(&ctx)); @@ -624,23 +453,17 @@ void testChunkedSplitInsideString() { ASSERT_EQ(ctx.events.size(), reference.size()); for (size_t i = 0; i < reference.size(); ++i) { - ASSERT_EQ(ctx.events[i].type, reference[i].type); - ASSERT_EQ(ctx.events[i].value, reference[i].value); + EXPECT_EQ(ctx.events[i].type, reference[i].type); + EXPECT_EQ(ctx.events[i].value, reference[i].value); } - - printf(" passed\n"); - PASS(); } -void testChunkedSplitInsideEscape() { - printf("testChunkedSplitInsideEscape...\n"); - +TEST(StreamingJsonParser, ChunkedSplitInsideEscape) { const char* json = R"({"k": "a\"b"})"; auto reference = parse(json); - // Find the backslash position and split right after it const char* bs = strchr(json + 7, '\\'); - ASSERT_TRUE(bs != nullptr); + ASSERT_NE(bs, nullptr); size_t splitAt = static_cast(bs - json) + 1; // after the backslash TestContext ctx; @@ -650,22 +473,16 @@ void testChunkedSplitInsideEscape() { ASSERT_EQ(ctx.events.size(), reference.size()); for (size_t i = 0; i < reference.size(); ++i) { - ASSERT_EQ(ctx.events[i].type, reference[i].type); - ASSERT_EQ(ctx.events[i].value, reference[i].value); + EXPECT_EQ(ctx.events[i].type, reference[i].type); + EXPECT_EQ(ctx.events[i].value, reference[i].value); } - - printf(" passed\n"); - PASS(); } -void testChunkedSplitInsideLiteral() { - printf("testChunkedSplitInsideLiteral...\n"); - +TEST(StreamingJsonParser, ChunkedSplitInsideLiteral) { const char* json = R"({"a": true, "b": false, "c": null})"; auto reference = parse(json); - // Split inside "true" (at "tr|ue") - size_t splitAt = 7; + size_t splitAt = 7; // inside "true" TestContext ctx; StreamingJsonParser parser(makeCallbacks(&ctx)); parser.feed(json, splitAt); @@ -673,65 +490,17 @@ void testChunkedSplitInsideLiteral() { ASSERT_EQ(ctx.events.size(), reference.size()); for (size_t i = 0; i < reference.size(); ++i) { - ASSERT_EQ(ctx.events[i].type, reference[i].type); - ASSERT_EQ(ctx.events[i].value, reference[i].value); + EXPECT_EQ(ctx.events[i].type, reference[i].type); + EXPECT_EQ(ctx.events[i].value, reference[i].value); } - - printf(" passed\n"); - PASS(); } -void testNullCallbacksNoCrash() { - printf("testNullCallbacksNoCrash...\n"); - +TEST(StreamingJsonParser, NullCallbacksNoCrash) { JsonCallbacks nullCbs = {}; nullCbs.ctx = nullptr; StreamingJsonParser parser(nullCbs); const char* json = R"({"key": "value", "num": 42, "b": true, "n": null, "a": [1]})"; parser.feed(json, strlen(json)); - ASSERT_TRUE(!parser.hasError()); - - printf(" passed\n"); - PASS(); -} - -// ============================================================================ - -int main() { - printf("=== StreamingJsonParser Tests ===\n\n"); - - testSimpleObject(); - testNestedObjects(); - testArrayOfValues(); - testArrayOfObjects(); - testStringEscapes(); - testUnicodeEscapePassthrough(); - testNumbers(); - testBooleansAndNull(); - testChunkedFeeding(); - testEveryByteBoundary(); - testLargeTokenTruncation(); - testEmptyObject(); - testEmptyArray(); - testNestedArrays(); - testTopLevelArray(); - testWhitespaceVariants(); - testResetBetweenDocuments(); - testNumberAtEndOfInput(); - testArrayOfStrings(); - testTruncatedInputNoCrash(); - testAllEscapeSequences(); - testObjectInArray(); - testDeeplyNested(); - testNestingOverflow(); - testNumberZero(); - testMultipleValuesInObject(); - testChunkedSplitInsideString(); - testChunkedSplitInsideEscape(); - testChunkedSplitInsideLiteral(); - testNullCallbacksNoCrash(); - - printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed); - return testsFailed > 0 ? 1 : 0; + EXPECT_FALSE(parser.hasError()); }