test: Migrate unit tests to gtest, integrate with CI (#2144)

This commit is contained in:
Zach Nelson
2026-05-25 15:51:53 -07:00
committed by GitHub
parent 4ee406897b
commit 6dbfc29c7e
17 changed files with 696 additions and 1368 deletions
+44
View File
@@ -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)
+12 -8
View File
@@ -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.
+17
View File
@@ -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)
@@ -1,34 +1,11 @@
#include <cassert>
#include <gtest/gtest.h>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#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<int32_t>(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);
}
+24
View File
@@ -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)
@@ -1,10 +1,8 @@
#include <Utf8.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <fstream>
#include <functional>
#include <iostream>
#include <sstream>
#include <string>
@@ -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<LanguageConfig> 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<size_t> expectedPositionsFromAnnotatedWord(const std::string& annotated) {
std::vector<size_t> positions;
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(annotated.c_str());
@@ -72,7 +59,6 @@ std::vector<TestCase> 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<TestCase> 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<siz
std::vector<size_t> hyphenateWordWithHyphenator(const std::string& word, const LanguageHyphenator& hyphenator) {
auto cps = collectCodepoints(word);
trimSurroundingPunctuationAndFootnote(cps);
return hyphenator.breakIndexes(cps);
}
std::vector<LanguageConfig> 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<std::vector<size_t>(const std::string&)> hyphenateFunc) {
EvaluationResult evaluateWord(const TestCase& testCase, const std::vector<size_t>& actualPositions) {
EvaluationResult result;
std::vector<size_t> actualPositions = hyphenateFunc(testCase.word);
std::vector<size_t> expected = testCase.expectedPositions;
std::vector<size_t> 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<int>(expected.size() * fpPenalty);
if (totalPossible > 0) {
result.weightedScore = 1.0 - (static_cast<double>(totalErrors) / totalPossible);
@@ -213,180 +176,59 @@ EvaluationResult evaluateWord(const TestCase& testCase,
return result;
}
void printResults(const std::string& language, const std::vector<TestCase>& testCases,
const std::vector<std::pair<TestCase, EvaluationResult>>& worstCases, int perfectMatches,
int partialMatches, int completeMisses, double totalPrecision, double totalRecall, double totalF1,
double totalWeighted, int totalTP, int totalFP, int totalFN,
std::function<std::vector<size_t>(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<TestCase> 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<std::pair<TestCase, EvaluationResult>> 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<double>(totalTP) / (totalTP + totalFP) : 0.0;
double overallRecall = totalTP + totalFN > 0 ? static_cast<double>(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<std::pair<TestCase, EvaluationResult>> 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<size_t> 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<int>(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<size_t> 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<int>(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<size_t> 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<LanguageConfig> 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<TestCase> 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<std::pair<TestCase, EvaluationResult>> 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<int>(10, static_cast<int>(imperfect.size()));
for (int i = 0; i < show; ++i) {
const TestCase& tc = imperfect[i].first;
std::vector<size_t> 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); }
+16
View File
@@ -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)
+156 -389
View File
@@ -1,51 +1,13 @@
#include <cassert>
#include <cstdio>
#include <gtest/gtest.h>
#include <cstring>
#include <string>
#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;
}
-30
View File
@@ -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" "$@"
-32
View File
@@ -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" "$@"
-29
View File
@@ -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" "$@"
-28
View File
@@ -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" "$@"
+15
View File
@@ -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)
@@ -1,37 +1,13 @@
#include <cassert>
#include <cstdio>
#include <gtest/gtest.h>
#include <cstring>
#include <string>
#include <vector>
#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<Event> events;
};
static void onKey(void* ctx, const char* key, size_t len) {
void onKey(void* ctx, const char* key, size_t len) {
static_cast<TestContext*>(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<TestContext*>(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<TestContext*>(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<TestContext*>(ctx)->events.push_back({value ? EventType::BOOL_TRUE : EventType::BOOL_FALSE, {}});
}
static void onNull(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::NULL_VAL, {}}); }
static void onObjectStart(void* ctx) {
static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_START, {}});
}
static void onObjectEnd(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_END, {}}); }
static void onArrayStart(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::ARRAY_START, {}}); }
static void onArrayEnd(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::ARRAY_END, {}}); }
void onNull(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::NULL_VAL, {}}); }
void onObjectStart(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_START, {}}); }
void onObjectEnd(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_END, {}}); }
void onArrayStart(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::ARRAY_START, {}}); }
void onArrayEnd(void* ctx) { static_cast<TestContext*>(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<Event> parse(const char* json) {
std::vector<Event> 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<Event> parseBytewise(const char* json) {
std::vector<Event> parseBytewise(const char* json) {
TestContext ctx;
StreamingJsonParser parser(makeCallbacks(&ctx));
size_t len = strlen(json);
@@ -97,176 +69,132 @@ static std::vector<Event> 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<size_t>(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());
}