test: Migrate unit tests to gtest, integrate with CI (#2144)
This commit is contained in:
@@ -102,6 +102,33 @@ jobs:
|
|||||||
path: .pio/build/default/firmware.bin
|
path: .pio/build/default/firmware.bin
|
||||||
if-no-files-found: error
|
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
|
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
|
||||||
# PR requirements.
|
# PR requirements.
|
||||||
test-status:
|
test-status:
|
||||||
@@ -110,6 +137,7 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
- clang-format
|
- clang-format
|
||||||
- cppcheck
|
- cppcheck
|
||||||
|
- unit-tests
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ extra_scripts =
|
|||||||
pre:scripts/gen_i18n.py
|
pre:scripts/gen_i18n.py
|
||||||
pre:scripts/git_branch.py
|
pre:scripts/git_branch.py
|
||||||
pre:scripts/patch_jpegdec.py
|
pre:scripts/patch_jpegdec.py
|
||||||
|
post:scripts/register_unit_tests_target.py
|
||||||
|
|
||||||
; Libraries
|
; Libraries
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
|||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -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
@@ -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
|
cmake -S test -B build/test
|
||||||
source code, sets of one or more MCU program modules together with associated
|
cmake --build build/test
|
||||||
control data, usage procedures, and operating procedures, are tested to
|
ctest --test-dir build/test --output-on-failure -j
|
||||||
determine whether they are fit for use. Unit testing finds problems early
|
|
||||||
in the development cycle.
|
|
||||||
|
|
||||||
More information about PlatformIO Unit Testing:
|
Run a single suite directly:
|
||||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -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 <cmath>
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "lib/EpdFont/EpdFont.h"
|
#include "lib/EpdFont/EpdFont.h"
|
||||||
#include "lib/EpdFont/EpdFontData.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
|
// Synthetic test font
|
||||||
//
|
//
|
||||||
@@ -43,8 +20,10 @@ static int testsFailed = 0;
|
|||||||
// o->a: -2 (-0.125px) o->o: -3 (-0.1875px)
|
// o->a: -2 (-0.125px) o->o: -3 (-0.1875px)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const EpdGlyph kGlyphs[] = {
|
const EpdGlyph kGlyphs[] = {
|
||||||
// idx width height advanceX left top dataLength dataOffset
|
// idx width height advanceX left top dataLength dataOffset
|
||||||
/* 0 'T' */ { 8, 12, 137, 0, 12, 0, 0 },
|
/* 0 'T' */ { 8, 12, 137, 0, 12, 0, 0 },
|
||||||
/* 1 'a' */ { 7, 8, 130, 0, 8, 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 },
|
/* 3 'x' */ { 7, 8, 136, 0, 8, 0, 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
static const EpdUnicodeInterval kIntervals[] = {
|
const EpdUnicodeInterval kIntervals[] = {
|
||||||
{ 0x54, 0x54, 0 }, // 'T' -> glyph[0]
|
{ 0x54, 0x54, 0 }, // 'T' -> glyph[0]
|
||||||
{ 0x61, 0x61, 1 }, // 'a' -> glyph[1]
|
{ 0x61, 0x61, 1 }, // 'a' -> glyph[1]
|
||||||
{ 0x6F, 0x6F, 2 }, // 'o' -> glyph[2]
|
{ 0x6F, 0x6F, 2 }, // 'o' -> glyph[2]
|
||||||
{ 0x78, 0x78, 3 }, // 'x' -> glyph[3]
|
{ 0x78, 0x78, 3 }, // 'x' -> glyph[3]
|
||||||
};
|
};
|
||||||
|
|
||||||
static const EpdKernClassEntry kKernLeft[] = {
|
const EpdKernClassEntry kKernLeft[] = {
|
||||||
{ 0x54, 1 }, // 'T' -> left class 1
|
{ 0x54, 1 }, // 'T' -> left class 1
|
||||||
{ 0x6F, 2 }, // 'o' -> left class 2
|
{ 0x6F, 2 }, // 'o' -> left class 2
|
||||||
};
|
};
|
||||||
|
|
||||||
static const EpdKernClassEntry kKernRight[] = {
|
const EpdKernClassEntry kKernRight[] = {
|
||||||
{ 0x61, 1 }, // 'a' -> right class 1
|
{ 0x61, 1 }, // 'a' -> right class 1
|
||||||
{ 0x6F, 2 }, // 'o' -> right class 2
|
{ 0x6F, 2 }, // 'o' -> right class 2
|
||||||
};
|
};
|
||||||
|
|
||||||
// Flat matrix: leftClassCount(2) x rightClassCount(2), 4.4 fixed-point
|
// 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)
|
// [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,
|
.bitmap = nullptr,
|
||||||
.glyph = kGlyphs,
|
.glyph = kGlyphs,
|
||||||
.intervals = kIntervals,
|
.intervals = kIntervals,
|
||||||
@@ -94,62 +73,63 @@ static const EpdFontData kTestFontData = {
|
|||||||
.kernRightClassCount = 2,
|
.kernRightClassCount = 2,
|
||||||
.ligaturePairs = nullptr,
|
.ligaturePairs = nullptr,
|
||||||
.ligaturePairCount = 0,
|
.ligaturePairCount = 0,
|
||||||
|
.glyphMissHandler = nullptr,
|
||||||
|
.glyphMissCtx = nullptr,
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
static EpdFont testFont(&kTestFontData);
|
EpdFont& testFont() {
|
||||||
|
static EpdFont font(&kTestFontData);
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: return width from getTextDimensions
|
int textWidth(const char* str) {
|
||||||
static int textWidth(const char* str) {
|
|
||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
testFont.getTextDimensions(str, &w, &h);
|
testFont().getTextDimensions(str, &w, &h);
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int textHeight(const char* str) {
|
int textHeight(const char* str) {
|
||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
testFont.getTextDimensions(str, &w, &h);
|
testFont().getTextDimensions(str, &w, &h);
|
||||||
return 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
|
// Part 1: Pure fp4 math tests
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Simulate the old absolute-snap gap for comparison
|
TEST(Fp4Math, RoundTripIntegerPixels) {
|
||||||
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");
|
|
||||||
|
|
||||||
for (int px = 0; px < 500; px++) {
|
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() {
|
TEST(Fp4Math, RoundingBoundaries) {
|
||||||
printf("testOldApproachInconsistency...\n");
|
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)
|
// 'oo' pair: advance=145 (9.0625px), kern=-3 (-0.1875px), combined=142 (8.875px)
|
||||||
const int32_t advance = 145;
|
const int32_t advance = 145;
|
||||||
const int32_t kern = -3;
|
const int32_t kern = -3;
|
||||||
@@ -164,76 +144,51 @@ void testOldApproachInconsistency() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ASSERT_TRUE(maxGap - minGap >= 1);
|
// Absolute snap produces inconsistent gaps depending on subpixel phase.
|
||||||
printf(" Old absolute gap range: [%d, %d] -- varies by %d px\n", minGap, maxGap, maxGap - minGap);
|
EXPECT_GE(maxGap - minGap, 1);
|
||||||
|
|
||||||
int diffStep = fp4::toPixel(advance + kern);
|
|
||||||
printf(" Differential step: always %d px\n", diffStep);
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testExhaustiveKernRange() {
|
TEST(Fp4Math, ExhaustiveKernRange) {
|
||||||
printf("testExhaustiveKernRange...\n");
|
|
||||||
|
|
||||||
const int32_t baseAdvance = 128;
|
const int32_t baseAdvance = 128;
|
||||||
int checked = 0;
|
|
||||||
|
|
||||||
for (int advFrac = 0; advFrac < 16; advFrac++) {
|
for (int advFrac = 0; advFrac < 16; advFrac++) {
|
||||||
int32_t advance = baseAdvance + advFrac;
|
int32_t advance = baseAdvance + advFrac;
|
||||||
for (int kern = -128; kern <= 127; kern++) {
|
for (int kern = -128; kern <= 127; kern++) {
|
||||||
int step = fp4::toPixel(advance + static_cast<int32_t>(kern));
|
int step = fp4::toPixel(advance + static_cast<int32_t>(kern));
|
||||||
float idealPx = fp4::toFloat(advance + kern);
|
float idealPx = fp4::toFloat(advance + kern);
|
||||||
if (std::abs(step - idealPx) >= 1.0f) {
|
EXPECT_LT(std::abs(step - idealPx), 1.0f) << "advance=" << advance << " kern=" << kern;
|
||||||
fprintf(stderr, " FAIL: advance=%d, kern=%d, step=%d, ideal=%.4f\n", advance, kern, step, idealPx);
|
|
||||||
testsFailed++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
checked++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" Checked %d (advance, kern) combinations -- all within 1px of ideal\n", checked);
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Part 2: Integration tests using real EpdFont::getTextDimensions
|
// Part 2: Integration tests using real EpdFont::getTextDimensions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void testKernLookup() {
|
TEST(EpdFont, KernLookup) {
|
||||||
printf("testKernLookup...\n");
|
EXPECT_EQ(testFont().getKerning('T', 'a'), -5);
|
||||||
|
EXPECT_EQ(testFont().getKerning('T', 'o'), -7);
|
||||||
ASSERT_EQ(testFont.getKerning('T', 'a'), -5);
|
EXPECT_EQ(testFont().getKerning('o', 'a'), -2);
|
||||||
ASSERT_EQ(testFont.getKerning('T', 'o'), -7);
|
EXPECT_EQ(testFont().getKerning('o', 'o'), -3);
|
||||||
ASSERT_EQ(testFont.getKerning('o', 'a'), -2);
|
EXPECT_EQ(testFont().getKerning('a', 'o'), 0); // 'a' has no left class
|
||||||
ASSERT_EQ(testFont.getKerning('o', 'o'), -3);
|
EXPECT_EQ(testFont().getKerning('x', 'o'), 0); // 'x' has no left class
|
||||||
ASSERT_EQ(testFont.getKerning('a', 'o'), 0); // 'a' has no left class
|
EXPECT_EQ(testFont().getKerning('T', 'x'), 0); // 'x' has no right class
|
||||||
ASSERT_EQ(testFont.getKerning('x', 'o'), 0); // 'x' has no left class
|
EXPECT_EQ(testFont().getKerning('T', 'T'), 0); // 'T' has no right 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testGlyphLookup() {
|
TEST(EpdFont, GlyphLookup) {
|
||||||
printf("testGlyphLookup...\n");
|
ASSERT_NE(testFont().getGlyph('T'), nullptr);
|
||||||
|
ASSERT_NE(testFont().getGlyph('a'), nullptr);
|
||||||
ASSERT_TRUE(testFont.getGlyph('T') != nullptr);
|
ASSERT_NE(testFont().getGlyph('o'), nullptr);
|
||||||
ASSERT_TRUE(testFont.getGlyph('a') != nullptr);
|
ASSERT_NE(testFont().getGlyph('x'), nullptr);
|
||||||
ASSERT_TRUE(testFont.getGlyph('o') != nullptr);
|
EXPECT_EQ(testFont().getGlyph('T')->advanceX, 137);
|
||||||
ASSERT_TRUE(testFont.getGlyph('x') != nullptr);
|
EXPECT_EQ(testFont().getGlyph('a')->advanceX, 130);
|
||||||
ASSERT_EQ(testFont.getGlyph('T')->advanceX, 137);
|
EXPECT_EQ(testFont().getGlyph('o')->advanceX, 145);
|
||||||
ASSERT_EQ(testFont.getGlyph('a')->advanceX, 130);
|
EXPECT_EQ(testFont().getGlyph('x')->advanceX, 136);
|
||||||
ASSERT_EQ(testFont.getGlyph('o')->advanceX, 145);
|
|
||||||
ASSERT_EQ(testFont.getGlyph('x')->advanceX, 136);
|
|
||||||
|
|
||||||
// No U+FFFD in font, so unknown codepoints return nullptr
|
// No U+FFFD in font, so unknown codepoints return nullptr
|
||||||
ASSERT_TRUE(testFont.getGlyph('Z') == nullptr);
|
EXPECT_EQ(testFont().getGlyph('Z'), nullptr);
|
||||||
ASSERT_TRUE(testFont.getGlyph('b') == nullptr);
|
EXPECT_EQ(testFont().getGlyph('b'), nullptr);
|
||||||
|
|
||||||
printf(" All glyph lookups correct\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Known-value regression tests. Expected widths are computed by hand using
|
// 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:
|
// Differential step from glyph A to glyph B:
|
||||||
// step = fp4::toPixel(advanceA + kern(A,B))
|
// step = fp4::toPixel(advanceA + kern(A,B))
|
||||||
void testKnownWidths() {
|
TEST(EpdFont, KnownWidths) {
|
||||||
printf("testKnownWidths...\n");
|
// "o": single glyph at x=0, width=8 -> w = 0 + 8 = 8
|
||||||
|
EXPECT_EQ(textWidth("o"), 8);
|
||||||
// "o": single glyph at x=0, width=8
|
|
||||||
// w = 0 + 8 = 8
|
|
||||||
ASSERT_EQ(textWidth("o"), 8);
|
|
||||||
|
|
||||||
// "oo": step = toPixel(145 + (-3)) = toPixel(142) = 9
|
// "oo": step = toPixel(145 + (-3)) = toPixel(142) = 9
|
||||||
// o1 at 0, o2 at 9. w = 9 + 8 = 17
|
// 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
|
// "ooo": two steps of 9 -> o3 at 18, w = 18 + 8 = 26
|
||||||
// o1 at 0, o2 at 9, o3 at 18. w = 18 + 8 = 26
|
EXPECT_EQ(textWidth("ooo"), 26);
|
||||||
ASSERT_EQ(textWidth("ooo"), 26);
|
|
||||||
|
|
||||||
// "To": step = toPixel(137 + (-7)) = toPixel(130) = 8
|
// "To": step = toPixel(137 + (-7)) = 8 -> o at 8, w = 8 + 8 = 16
|
||||||
// T at 0, o at 8. w = 8 + 8 = 16
|
EXPECT_EQ(textWidth("To"), 16);
|
||||||
ASSERT_EQ(textWidth("To"), 16);
|
|
||||||
|
|
||||||
// "Ta": step = toPixel(137 + (-5)) = toPixel(132) = 8
|
// "Ta": step = toPixel(137 + (-5)) = 8 -> a at 8, w = 8 + 7 = 15
|
||||||
// T at 0, a at 8. w = 8 + 7 = 15
|
EXPECT_EQ(textWidth("Ta"), 15);
|
||||||
ASSERT_EQ(textWidth("Ta"), 15);
|
|
||||||
|
|
||||||
// "oa": step = toPixel(145 + (-2)) = toPixel(143) = 9
|
// "oa": step = toPixel(145 + (-2)) = 9 -> a at 9, w = 9 + 7 = 16
|
||||||
// o at 0, a at 9. w = 9 + 7 = 16
|
EXPECT_EQ(textWidth("oa"), 16);
|
||||||
ASSERT_EQ(textWidth("oa"), 16);
|
|
||||||
|
|
||||||
// "Too": T at 0.
|
// "Too": T at 0, o1 at 8 (T->o step), o2 at 17 (o->o step). w = 17 + 8 = 25
|
||||||
// step T->o = toPixel(137 + (-7)) = 8. o1 at 8.
|
EXPECT_EQ(textWidth("Too"), 25);
|
||||||
// step o->o = toPixel(145 + (-3)) = 9. o2 at 17.
|
|
||||||
// w = 17 + 8 = 25
|
|
||||||
ASSERT_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
|
// x at 0, o at 9. w = 9 + 8 = 17
|
||||||
ASSERT_EQ(textWidth("xo"), 17);
|
EXPECT_EQ(textWidth("xo"), 17);
|
||||||
|
|
||||||
printf(" All known widths correct\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "oo" pair consistency: the pixel gap between two o's must be the same
|
// "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
|
// differential rounding. With absolute snapping, "xoo" would produce a
|
||||||
// different oo gap than "oo" because 'x' advance (136 FP) puts the first
|
// different oo gap than "oo" because 'x' advance (136 FP) puts the first
|
||||||
// 'o' at fractional phase 8, crossing the rounding boundary differently.
|
// 'o' at fractional phase 8, crossing the rounding boundary differently.
|
||||||
void testPairConsistencyViaFont() {
|
TEST(EpdFont, PairConsistencyViaFont) {
|
||||||
printf("testPairConsistencyViaFont...\n");
|
|
||||||
|
|
||||||
// The oo gap = width(prefix + "oo") - width(prefix + "o")
|
// The oo gap = width(prefix + "oo") - width(prefix + "o")
|
||||||
// This isolates the pixel distance contributed by the second 'o'.
|
// This isolates the pixel distance contributed by the second 'o'.
|
||||||
const int oo_gap_bare = textWidth("oo") - textWidth("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_T = textWidth("Too") - textWidth("To");
|
||||||
const int oo_gap_after_o = textWidth("ooo") - textWidth("oo");
|
const int oo_gap_after_o = textWidth("ooo") - textWidth("oo");
|
||||||
|
|
||||||
printf(" oo gap (bare): %d\n", oo_gap_bare);
|
EXPECT_EQ(oo_gap_after_x, oo_gap_bare);
|
||||||
printf(" oo gap (after x): %d\n", oo_gap_after_x);
|
EXPECT_EQ(oo_gap_after_T, oo_gap_bare);
|
||||||
printf(" oo gap (after T): %d\n", oo_gap_after_T);
|
EXPECT_EQ(oo_gap_after_o, oo_gap_bare);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Null-glyph handling: when a codepoint has no glyph (and no replacement
|
// Null-glyph handling: when a codepoint has no glyph (and no replacement
|
||||||
// glyph), the pending advance from the previous glyph must still be flushed.
|
// 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.
|
// Without the flush fix, the glyph after the null would overlap the one before.
|
||||||
void testNullGlyphAdvancePreserved() {
|
TEST(EpdFont, NullGlyphAdvancePreserved) {
|
||||||
printf("testNullGlyphAdvancePreserved...\n");
|
|
||||||
|
|
||||||
// 'Z' (0x5A) is not in our font and there's no U+FFFD, so getGlyph returns null.
|
// '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.
|
// "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).
|
// toPixel(145) = 9 (o's advance, no kern since Z resets prevCp).
|
||||||
// w = 9 + 8 = 17
|
// w = 9 + 8 = 17
|
||||||
int w = textWidth("oZo");
|
EXPECT_EQ(textWidth("oZo"), 17);
|
||||||
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);
|
|
||||||
|
|
||||||
// Multi-null: "oZZo" -- two consecutive nulls, advance still preserved.
|
// Multi-null: "oZZo" -- two consecutive nulls, advance still preserved.
|
||||||
w = textWidth("oZZo");
|
EXPECT_EQ(textWidth("oZZo"), 17);
|
||||||
printf(" width(\"oZZo\") = %d\n", w);
|
|
||||||
ASSERT_EQ(w, 17);
|
|
||||||
|
|
||||||
// Null at start: "Zo" -- no pending advance to flush, o renders at 0.
|
// Null at start: "Zo" -- no pending advance to flush, o renders at 0.
|
||||||
w = textWidth("Zo");
|
EXPECT_EQ(textWidth("Zo"), 8);
|
||||||
printf(" width(\"Zo\") = %d\n", w);
|
|
||||||
ASSERT_EQ(w, 8);
|
|
||||||
|
|
||||||
printf(" Null-glyph advance correctly preserved\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testHeightCalculation() {
|
TEST(EpdFont, HeightCalculation) {
|
||||||
printf("testHeightCalculation...\n");
|
|
||||||
|
|
||||||
// 'T' is tallest: top=12, height=12 -> extent [0, 12)
|
// 'T' is tallest: top=12, height=12 -> extent [0, 12)
|
||||||
// 'o' and 'a': top=8, height=8 -> extent [0, 8)
|
// 'o' and 'a': top=8, height=8 -> extent [0, 8)
|
||||||
ASSERT_EQ(textHeight("o"), 8);
|
EXPECT_EQ(textHeight("o"), 8);
|
||||||
ASSERT_EQ(textHeight("T"), 12);
|
EXPECT_EQ(textHeight("T"), 12);
|
||||||
ASSERT_EQ(textHeight("To"), 12);
|
EXPECT_EQ(textHeight("To"), 12);
|
||||||
ASSERT_EQ(textHeight("oo"), 8);
|
EXPECT_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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <Utf8.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
|
||||||
#include <cmath>
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <functional>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -14,6 +12,12 @@
|
|||||||
#include "lib/Epub/Epub/hyphenation/LanguageHyphenator.h"
|
#include "lib/Epub/Epub/hyphenation/LanguageHyphenator.h"
|
||||||
#include "lib/Epub/Epub/hyphenation/LanguageRegistry.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 {
|
struct TestCase {
|
||||||
std::string word;
|
std::string word;
|
||||||
std::string hyphenated;
|
std::string hyphenated;
|
||||||
@@ -31,23 +35,6 @@ struct EvaluationResult {
|
|||||||
double weightedScore = 0.0;
|
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> expectedPositionsFromAnnotatedWord(const std::string& annotated) {
|
||||||
std::vector<size_t> positions;
|
std::vector<size_t> positions;
|
||||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(annotated.c_str());
|
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);
|
std::ifstream file(filename);
|
||||||
|
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
std::cerr << "Error: Could not open file " << filename << std::endl;
|
|
||||||
return testCases;
|
return testCases;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,14 +76,11 @@ std::vector<TestCase> loadTestData(const std::string& filename) {
|
|||||||
testCase.word = word;
|
testCase.word = word;
|
||||||
testCase.hyphenated = hyphenated;
|
testCase.hyphenated = hyphenated;
|
||||||
testCase.frequency = std::stoi(freqStr);
|
testCase.frequency = std::stoi(freqStr);
|
||||||
|
|
||||||
testCase.expectedPositions = expectedPositionsFromAnnotatedWord(hyphenated);
|
testCase.expectedPositions = expectedPositionsFromAnnotatedWord(hyphenated);
|
||||||
|
|
||||||
testCases.push_back(testCase);
|
testCases.push_back(testCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
|
||||||
return testCases;
|
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) {
|
std::vector<size_t> hyphenateWordWithHyphenator(const std::string& word, const LanguageHyphenator& hyphenator) {
|
||||||
auto cps = collectCodepoints(word);
|
auto cps = collectCodepoints(word);
|
||||||
trimSurroundingPunctuationAndFootnote(cps);
|
trimSurroundingPunctuationAndFootnote(cps);
|
||||||
|
|
||||||
return hyphenator.breakIndexes(cps);
|
return hyphenator.breakIndexes(cps);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<LanguageConfig> resolveLanguages(const std::string& selection) {
|
EvaluationResult evaluateWord(const TestCase& testCase, const std::vector<size_t>& actualPositions) {
|
||||||
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 result;
|
EvaluationResult result;
|
||||||
|
|
||||||
std::vector<size_t> actualPositions = hyphenateFunc(testCase.word);
|
|
||||||
|
|
||||||
std::vector<size_t> expected = testCase.expectedPositions;
|
std::vector<size_t> expected = testCase.expectedPositions;
|
||||||
std::vector<size_t> actual = actualPositions;
|
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);
|
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
|
// Treat words with no expected and no actual hyphenation marks as perfect.
|
||||||
// algorithmic output as perfect matches so they don't drag down the per-word averages.
|
|
||||||
if (expected.empty() && actual.empty()) {
|
if (expected.empty() && actual.empty()) {
|
||||||
result.precision = 1.0;
|
result.precision = 1.0;
|
||||||
result.recall = 1.0;
|
result.recall = 1.0;
|
||||||
@@ -199,9 +163,8 @@ EvaluationResult evaluateWord(const TestCase& testCase,
|
|||||||
|
|
||||||
double fpPenalty = 2.0;
|
double fpPenalty = 2.0;
|
||||||
double fnPenalty = 1.0;
|
double fnPenalty = 1.0;
|
||||||
|
|
||||||
int totalErrors = result.falsePositives * fpPenalty + result.falseNegatives * fnPenalty;
|
int totalErrors = result.falsePositives * fpPenalty + result.falseNegatives * fnPenalty;
|
||||||
int totalPossible = expected.size() * fpPenalty;
|
int totalPossible = static_cast<int>(expected.size() * fpPenalty);
|
||||||
|
|
||||||
if (totalPossible > 0) {
|
if (totalPossible > 0) {
|
||||||
result.weightedScore = 1.0 - (static_cast<double>(totalErrors) / totalPossible);
|
result.weightedScore = 1.0 - (static_cast<double>(totalErrors) / totalPossible);
|
||||||
@@ -213,180 +176,59 @@ EvaluationResult evaluateWord(const TestCase& testCase,
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void printResults(const std::string& language, const std::vector<TestCase>& testCases,
|
// Runs the evaluation for a single language and asserts the per-word average F1
|
||||||
const std::vector<std::pair<TestCase, EvaluationResult>>& worstCases, int perfectMatches,
|
// is at or above `minF1Percent`. Thresholds are set ~1pp below measured
|
||||||
int partialMatches, int completeMisses, double totalPrecision, double totalRecall, double totalF1,
|
// baselines so unrelated tweaks don't fail CI but real regressions still trip.
|
||||||
double totalWeighted, int totalTP, int totalFP, int totalFN,
|
void runLanguageEval(const char* langName, const char* primaryTag, const char* resourceFile, double minF1Percent) {
|
||||||
std::function<std::vector<size_t>(const std::string&)> hyphenateFunc) {
|
const auto* hyphenator = getLanguageHyphenatorForPrimaryTag(primaryTag);
|
||||||
std::string lang_upper = language;
|
ASSERT_NE(hyphenator, nullptr) << "No hyphenator registered for tag: " << primaryTag;
|
||||||
if (!lang_upper.empty()) {
|
|
||||||
lang_upper[0] = std::toupper(lang_upper[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "================================================================================" << std::endl;
|
std::string path = std::string(HYPHENATION_RESOURCES_DIR) + "/" + resourceFile;
|
||||||
std::cout << lang_upper << " HYPHENATION EVALUATION RESULTS" << std::endl;
|
std::vector<TestCase> testCases = loadTestData(path);
|
||||||
std::cout << "================================================================================" << std::endl;
|
ASSERT_FALSE(testCases.empty()) << "No test cases loaded from " << path;
|
||||||
std::cout << std::endl;
|
|
||||||
|
|
||||||
std::cout << "Total test cases: " << testCases.size() << std::endl;
|
double totalF1 = 0.0;
|
||||||
std::cout << "Perfect matches: " << perfectMatches << " (" << (perfectMatches * 100.0 / testCases.size()) << "%)"
|
std::vector<std::pair<TestCase, EvaluationResult>> imperfect;
|
||||||
<< std::endl;
|
|
||||||
std::cout << "Partial matches: " << partialMatches << std::endl;
|
|
||||||
std::cout << "Complete misses: " << completeMisses << std::endl;
|
|
||||||
std::cout << std::endl;
|
|
||||||
|
|
||||||
std::cout << "--- Overall Metrics (averaged per word) ---" << std::endl;
|
for (const auto& tc : testCases) {
|
||||||
std::cout << "Average Precision: " << (totalPrecision / testCases.size() * 100.0) << "%" << std::endl;
|
std::vector<size_t> actual = hyphenateWordWithHyphenator(tc.word, *hyphenator);
|
||||||
std::cout << "Average Recall: " << (totalRecall / testCases.size() * 100.0) << "%" << std::endl;
|
EvaluationResult res = evaluateWord(tc, actual);
|
||||||
std::cout << "Average F1 Score: " << (totalF1 / testCases.size() * 100.0) << "%" << std::endl;
|
totalF1 += res.f1Score;
|
||||||
std::cout << "Average Weighted Score: " << (totalWeighted / testCases.size() * 100.0) << "% (FP penalty: 2x)"
|
if (res.weightedScore < 0.999999) {
|
||||||
<< std::endl;
|
imperfect.emplace_back(tc, res);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "--- Worst Cases (lowest weighted scores) ---" << std::endl;
|
double averageF1Percent = totalF1 / testCases.size() * 100.0;
|
||||||
int showCount = std::min(10, static_cast<int>(imperfectCases.size()));
|
::testing::Test::RecordProperty("avg_f1_percent", std::to_string(averageF1Percent));
|
||||||
for (int i = 0; i < showCount; i++) {
|
::testing::Test::RecordProperty("test_cases", std::to_string(testCases.size()));
|
||||||
const auto& testCase = imperfectCases[i].first;
|
|
||||||
const auto& result = imperfectCases[i].second;
|
|
||||||
|
|
||||||
std::vector<size_t> actualPositions = hyphenateFunc(testCase.word);
|
std::cout << langName << ": F1=" << averageF1Percent << "% (threshold " << minF1Percent << "%, " << testCases.size()
|
||||||
std::string actualHyphenated = positionsToHyphenated(testCase.word, actualPositions);
|
<< " cases)\n";
|
||||||
|
|
||||||
std::cout << "Word: " << testCase.word << " (freq: " << testCase.frequency << ")" << std::endl;
|
if (averageF1Percent < minF1Percent) {
|
||||||
std::cout << " Expected: " << testCase.hyphenated << std::endl;
|
std::sort(imperfect.begin(), imperfect.end(),
|
||||||
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(),
|
|
||||||
[](const auto& a, const auto& b) { return a.second.weightedScore < b.second.weightedScore; });
|
[](const auto& a, const auto& b) { return a.second.weightedScore < b.second.weightedScore; });
|
||||||
|
std::cout << "Worst cases for " << langName << ":\n";
|
||||||
printResults(lang.cliName, testCases, worstCases, perfectMatches, partialMatches, completeMisses, totalPrecision,
|
int show = std::min<int>(10, static_cast<int>(imperfect.size()));
|
||||||
totalRecall, totalF1, totalWeighted, totalTP, totalFP, totalFN, hyphenateFunc);
|
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); }
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,51 +1,13 @@
|
|||||||
#include <cassert>
|
#include <gtest/gtest.h>
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "lib/JsonParser/ReleaseJsonParser.h"
|
#include "lib/JsonParser/ReleaseJsonParser.h"
|
||||||
|
|
||||||
static int testsPassed = 0;
|
namespace {
|
||||||
static int testsFailed = 0;
|
|
||||||
|
|
||||||
#define ASSERT_EQ(a, b) \
|
const char* kRealisticPretty = R"({
|
||||||
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"({
|
|
||||||
"url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345",
|
"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",
|
"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}",
|
"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}})";
|
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
|
void feedChunked(ReleaseJsonParser& p, const char* json, size_t chunkSize) {
|
||||||
static void feedChunked(ReleaseJsonParser& p, const char* json, size_t chunkSize) {
|
|
||||||
size_t len = strlen(json);
|
size_t len = strlen(json);
|
||||||
for (size_t off = 0; off < len; off += chunkSize) {
|
for (size_t off = 0; off < len; off += chunkSize) {
|
||||||
size_t n = len - off < chunkSize ? 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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
} // namespace
|
||||||
// Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
void testRealisticPrettyPrinted() {
|
|
||||||
printf("testRealisticPrettyPrinted...\n");
|
|
||||||
|
|
||||||
|
TEST(ReleaseJsonParser, RealisticPrettyPrinted) {
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(kRealisticPretty, strlen(kRealisticPretty));
|
p.feed(kRealisticPretty, strlen(kRealisticPretty));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
EXPECT_STREQ(p.getTagName(), "v2.4.1");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(),
|
EXPECT_STREQ(p.getFirmwareUrl(),
|
||||||
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
EXPECT_EQ(p.getFirmwareSize(), 1572864u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testRealisticMinified() {
|
TEST(ReleaseJsonParser, RealisticMinified) {
|
||||||
printf("testRealisticMinified...\n");
|
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(kRealisticMinified, strlen(kRealisticMinified));
|
p.feed(kRealisticMinified, strlen(kRealisticMinified));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
EXPECT_STREQ(p.getTagName(), "v2.4.1");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(),
|
EXPECT_STREQ(p.getFirmwareUrl(),
|
||||||
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
EXPECT_EQ(p.getFirmwareSize(), 1572864u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testPrettyAndMinifiedAgree() {
|
TEST(ReleaseJsonParser, PrettyAndMinifiedAgree) {
|
||||||
printf("testPrettyAndMinifiedAgree...\n");
|
|
||||||
|
|
||||||
ReleaseJsonParser pretty;
|
ReleaseJsonParser pretty;
|
||||||
pretty.feed(kRealisticPretty, strlen(kRealisticPretty));
|
pretty.feed(kRealisticPretty, strlen(kRealisticPretty));
|
||||||
|
|
||||||
ReleaseJsonParser minified;
|
ReleaseJsonParser minified;
|
||||||
minified.feed(kRealisticMinified, strlen(kRealisticMinified));
|
minified.feed(kRealisticMinified, strlen(kRealisticMinified));
|
||||||
|
|
||||||
ASSERT_STREQ(pretty.getTagName(), minified.getTagName());
|
ASSERT_TRUE(pretty.foundTag());
|
||||||
ASSERT_STREQ(pretty.getFirmwareUrl(), minified.getFirmwareUrl());
|
ASSERT_TRUE(pretty.foundFirmware());
|
||||||
ASSERT_EQ(pretty.getFirmwareSize(), minified.getFirmwareSize());
|
ASSERT_TRUE(minified.foundTag());
|
||||||
|
ASSERT_TRUE(minified.foundFirmware());
|
||||||
|
|
||||||
printf(" passed\n");
|
EXPECT_STREQ(pretty.getTagName(), minified.getTagName());
|
||||||
PASS();
|
EXPECT_STREQ(pretty.getFirmwareUrl(), minified.getFirmwareUrl());
|
||||||
|
EXPECT_EQ(pretty.getFirmwareSize(), minified.getFirmwareSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFirmwareNotFirstAsset() {
|
TEST(ReleaseJsonParser, FirmwareNotFirstAsset) {
|
||||||
printf("testFirmwareNotFirstAsset...\n");
|
|
||||||
|
|
||||||
// firmware.bin is the third of four assets
|
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v1.0.0",
|
"tag_name": "v1.0.0",
|
||||||
"assets": [
|
"assets": [
|
||||||
@@ -231,19 +177,14 @@ void testFirmwareNotFirstAsset() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v1.0.0");
|
EXPECT_STREQ(p.getTagName(), "v1.0.0");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/firmware.bin");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/firmware.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 987654u);
|
EXPECT_EQ(p.getFirmwareSize(), 987654u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFieldOrderUrlBeforeName() {
|
TEST(ReleaseJsonParser, FieldOrderUrlBeforeName) {
|
||||||
printf("testFieldOrderUrlBeforeName...\n");
|
|
||||||
|
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v3.0",
|
"tag_name": "v3.0",
|
||||||
"assets": [{
|
"assets": [{
|
||||||
@@ -256,17 +197,12 @@ void testFieldOrderUrlBeforeName() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 2222u);
|
EXPECT_EQ(p.getFirmwareSize(), 2222u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFieldOrderSizeBeforeUrl() {
|
TEST(ReleaseJsonParser, FieldOrderSizeBeforeUrl) {
|
||||||
printf("testFieldOrderSizeBeforeUrl...\n");
|
|
||||||
|
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v3.1",
|
"tag_name": "v3.1",
|
||||||
"assets": [{
|
"assets": [{
|
||||||
@@ -279,17 +215,12 @@ void testFieldOrderSizeBeforeUrl() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw2.bin");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw2.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 3333u);
|
EXPECT_EQ(p.getFirmwareSize(), 3333u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFieldOrderNameFirst() {
|
TEST(ReleaseJsonParser, FieldOrderNameFirst) {
|
||||||
printf("testFieldOrderNameFirst...\n");
|
|
||||||
|
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v3.2",
|
"tag_name": "v3.2",
|
||||||
"assets": [{
|
"assets": [{
|
||||||
@@ -302,17 +233,12 @@ void testFieldOrderNameFirst() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw3.bin");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw3.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 4444u);
|
EXPECT_EQ(p.getFirmwareSize(), 4444u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testAssetsBeforeTagName() {
|
TEST(ReleaseJsonParser, AssetsBeforeTagName) {
|
||||||
printf("testAssetsBeforeTagName...\n");
|
|
||||||
|
|
||||||
// tag_name appears after assets in the JSON
|
// tag_name appears after assets in the JSON
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"name": "Release",
|
"name": "Release",
|
||||||
@@ -327,69 +253,51 @@ void testAssetsBeforeTagName() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v4.0");
|
EXPECT_STREQ(p.getTagName(), "v4.0");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 5555u);
|
EXPECT_EQ(p.getFirmwareSize(), 5555u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedFeedingRealisticSmallChunks() {
|
TEST(ReleaseJsonParser, ChunkedFeedingSmallChunks) {
|
||||||
printf("testChunkedFeedingRealisticSmallChunks...\n");
|
|
||||||
|
|
||||||
// Simulate HTTP chunked transfer with small chunks (64 bytes)
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
feedChunked(p, kRealisticPretty, 64);
|
feedChunked(p, kRealisticPretty, 64);
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
EXPECT_STREQ(p.getTagName(), "v2.4.1");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(),
|
EXPECT_STREQ(p.getFirmwareUrl(),
|
||||||
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
EXPECT_EQ(p.getFirmwareSize(), 1572864u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedFeedingByteByByte() {
|
TEST(ReleaseJsonParser, ChunkedFeedingByteByByte) {
|
||||||
printf("testChunkedFeedingByteByByte...\n");
|
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
feedChunked(p, kRealisticMinified, 1);
|
feedChunked(p, kRealisticMinified, 1);
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
EXPECT_STREQ(p.getTagName(), "v2.4.1");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
EXPECT_EQ(p.getFirmwareSize(), 1572864u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedFeedingVariousChunkSizes() {
|
TEST(ReleaseJsonParser, ChunkedFeedingVariousChunkSizes) {
|
||||||
printf("testChunkedFeedingVariousChunkSizes...\n");
|
for (size_t chunkSize : {3u, 7u, 13u, 31u, 97u, 128u, 256u, 512u, 1024u}) {
|
||||||
|
|
||||||
for (size_t chunkSize : {3, 7, 13, 31, 97, 128, 256, 512, 1024}) {
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
feedChunked(p, kRealisticPretty, chunkSize);
|
feedChunked(p, kRealisticPretty, chunkSize);
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag()) << "chunkSize=" << chunkSize;
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware()) << "chunkSize=" << chunkSize;
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
EXPECT_STREQ(p.getTagName(), "v2.4.1") << "chunkSize=" << chunkSize;
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
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() {
|
TEST(ReleaseJsonParser, MissingTagName) {
|
||||||
printf("testMissingTagName...\n");
|
|
||||||
|
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"name": "Some Release",
|
"name": "Some Release",
|
||||||
"draft": false,
|
"draft": false,
|
||||||
@@ -403,17 +311,12 @@ void testMissingTagName() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(!p.foundTag());
|
EXPECT_FALSE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "");
|
EXPECT_STREQ(p.getTagName(), "");
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testMissingFirmwareBinAsset() {
|
TEST(ReleaseJsonParser, MissingFirmwareBinAsset) {
|
||||||
printf("testMissingFirmwareBinAsset...\n");
|
|
||||||
|
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v1.0.0",
|
"tag_name": "v1.0.0",
|
||||||
"assets": [
|
"assets": [
|
||||||
@@ -425,129 +328,88 @@ void testMissingFirmwareBinAsset() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v1.0.0");
|
EXPECT_STREQ(p.getTagName(), "v1.0.0");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "");
|
EXPECT_STREQ(p.getFirmwareUrl(), "");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 0u);
|
EXPECT_EQ(p.getFirmwareSize(), 0u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testEmptyAssetsArray() {
|
TEST(ReleaseJsonParser, EmptyAssetsArray) {
|
||||||
printf("testEmptyAssetsArray...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"tag_name": "v1.0.0", "assets": []})";
|
const char* json = R"({"tag_name": "v1.0.0", "assets": []})";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNoAssetsKey() {
|
TEST(ReleaseJsonParser, NoAssetsKey) {
|
||||||
printf("testNoAssetsKey...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"tag_name": "v1.0.0", "name": "Release"})";
|
const char* json = R"({"tag_name": "v1.0.0", "name": "Release"})";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testTruncatedBeforeTagValue() {
|
TEST(ReleaseJsonParser, TruncatedBeforeTagValue) {
|
||||||
printf("testTruncatedBeforeTagValue...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"tag_name": )";
|
const char* json = R"({"tag_name": )";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(!p.foundTag());
|
EXPECT_FALSE(p.foundTag());
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testTruncatedInsideTagValue() {
|
TEST(ReleaseJsonParser, TruncatedInsideTagValue) {
|
||||||
printf("testTruncatedInsideTagValue...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"tag_name": "v2.4)";
|
const char* json = R"({"tag_name": "v2.4)";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(!p.foundTag());
|
EXPECT_FALSE(p.foundTag());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testTruncatedInsideAssetsArray() {
|
TEST(ReleaseJsonParser, TruncatedInsideAssetsArray) {
|
||||||
printf("testTruncatedInsideAssetsArray...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"tag_name": "v2.4.1", "assets": [{"name": "firm)";
|
const char* json = R"({"tag_name": "v2.4.1", "assets": [{"name": "firm)";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
EXPECT_STREQ(p.getTagName(), "v2.4.1");
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testTruncatedAfterFirmwareName() {
|
TEST(ReleaseJsonParser, TruncatedAfterFirmwareName) {
|
||||||
printf("testTruncatedAfterFirmwareName...\n");
|
|
||||||
|
|
||||||
// Found the name but connection dropped before URL/size
|
// Found the name but connection dropped before URL/size
|
||||||
const char* json = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_dow)";
|
const char* json = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_dow)";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testTruncatedRealisticJson() {
|
TEST(ReleaseJsonParser, TruncatedRealisticJson) {
|
||||||
printf("testTruncatedRealisticJson...\n");
|
|
||||||
|
|
||||||
// Truncate the realistic JSON at various points; none should crash
|
|
||||||
std::string full(kRealisticPretty);
|
std::string full(kRealisticPretty);
|
||||||
for (size_t cutPoint : {10u, 50u, 100u, 200u, 500u, 1000u, 1500u, 2000u}) {
|
for (size_t cutPoint : {10u, 50u, 100u, 200u, 500u, 1000u, 1500u, 2000u}) {
|
||||||
if (cutPoint >= full.size()) continue;
|
if (cutPoint >= full.size()) continue;
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(full.c_str(), cutPoint);
|
p.feed(full.c_str(), cutPoint);
|
||||||
// Just verify no crash; results depend on where we cut
|
|
||||||
(void)p.foundTag();
|
(void)p.foundTag();
|
||||||
(void)p.foundFirmware();
|
(void)p.foundFirmware();
|
||||||
}
|
}
|
||||||
|
SUCCEED();
|
||||||
printf(" passed (no crashes on truncated realistic JSON)\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNestedObjectsInAsset() {
|
TEST(ReleaseJsonParser, NestedObjectsInAsset) {
|
||||||
printf("testNestedObjectsInAsset...\n");
|
|
||||||
|
|
||||||
// Asset with deeply nested "uploader" object -- should not confuse depth tracking
|
// Asset with deeply nested "uploader" object -- should not confuse depth tracking
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v5.0",
|
"tag_name": "v5.0",
|
||||||
@@ -566,17 +428,12 @@ void testNestedObjectsInAsset() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw5.bin");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw5.bin");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 8888u);
|
EXPECT_EQ(p.getFirmwareSize(), 8888u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNestedObjectsAtTopLevel() {
|
TEST(ReleaseJsonParser, NestedObjectsAtTopLevel) {
|
||||||
printf("testNestedObjectsAtTopLevel...\n");
|
|
||||||
|
|
||||||
// Multiple nested objects at the top level before/after tag_name and assets
|
// Multiple nested objects at the top level before/after tag_name and assets
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"author": {"login": "dev", "id": 1, "nested": {"deep": true}},
|
"author": {"login": "dev", "id": 1, "nested": {"deep": true}},
|
||||||
@@ -589,18 +446,13 @@ void testNestedObjectsAtTopLevel() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v6.0");
|
EXPECT_STREQ(p.getTagName(), "v6.0");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1111u);
|
EXPECT_EQ(p.getFirmwareSize(), 1111u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testArraysAtTopLevel() {
|
TEST(ReleaseJsonParser, ArraysAtTopLevel) {
|
||||||
printf("testArraysAtTopLevel...\n");
|
|
||||||
|
|
||||||
// A non-assets array at the top level should not interfere
|
// A non-assets array at the top level should not interfere
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v7.0",
|
"tag_name": "v7.0",
|
||||||
@@ -611,69 +463,53 @@ void testArraysAtTopLevel() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v7.0");
|
EXPECT_STREQ(p.getTagName(), "v7.0");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 7070u);
|
EXPECT_EQ(p.getFirmwareSize(), 7070u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testResetAndReuse() {
|
TEST(ReleaseJsonParser, ResetAndReuse) {
|
||||||
printf("testResetAndReuse...\n");
|
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
|
|
||||||
const char* json1 =
|
const char* json1 =
|
||||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":1}]})";
|
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":1}]})";
|
||||||
p.feed(json1, strlen(json1));
|
p.feed(json1, strlen(json1));
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_STREQ(p.getTagName(), "v1.0");
|
EXPECT_STREQ(p.getTagName(), "v1.0");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://a");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://a");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1u);
|
EXPECT_EQ(p.getFirmwareSize(), 1u);
|
||||||
|
|
||||||
p.reset();
|
p.reset();
|
||||||
|
|
||||||
// Second document with different values
|
|
||||||
const char* json2 =
|
const char* json2 =
|
||||||
R"({"tag_name":"v2.0","assets":[{"name":"firmware.bin","browser_download_url":"https://b","size":2}]})";
|
R"({"tag_name":"v2.0","assets":[{"name":"firmware.bin","browser_download_url":"https://b","size":2}]})";
|
||||||
p.feed(json2, strlen(json2));
|
p.feed(json2, strlen(json2));
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.0");
|
EXPECT_STREQ(p.getTagName(), "v2.0");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://b");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://b");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 2u);
|
EXPECT_EQ(p.getFirmwareSize(), 2u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testResetClearsState() {
|
TEST(ReleaseJsonParser, ResetClearsState) {
|
||||||
printf("testResetClearsState...\n");
|
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
|
|
||||||
const char* json =
|
const char* json =
|
||||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":100}]})";
|
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":100}]})";
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
|
|
||||||
p.reset();
|
p.reset();
|
||||||
|
|
||||||
ASSERT_TRUE(!p.foundTag());
|
EXPECT_FALSE(p.foundTag());
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "");
|
EXPECT_STREQ(p.getTagName(), "");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "");
|
EXPECT_STREQ(p.getFirmwareUrl(), "");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 0u);
|
EXPECT_EQ(p.getFirmwareSize(), 0u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testPartialAssetNameMatch() {
|
TEST(ReleaseJsonParser, PartialAssetNameMatch) {
|
||||||
printf("testPartialAssetNameMatch...\n");
|
|
||||||
|
|
||||||
// "firmware.bin.bak" should NOT match "firmware.bin"
|
// "firmware.bin.bak" should NOT match "firmware.bin"
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v1.0",
|
"tag_name": "v1.0",
|
||||||
@@ -686,16 +522,11 @@ void testPartialAssetNameMatch() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(!p.foundFirmware());
|
EXPECT_FALSE(p.foundFirmware());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFirmwareBinExactMatch() {
|
TEST(ReleaseJsonParser, FirmwareBinExactMatch) {
|
||||||
printf("testFirmwareBinExactMatch...\n");
|
|
||||||
|
|
||||||
// Only exact "firmware.bin" matches, not similar names
|
// Only exact "firmware.bin" matches, not similar names
|
||||||
const char* json = R"({
|
const char* json = R"({
|
||||||
"tag_name": "v1.0",
|
"tag_name": "v1.0",
|
||||||
@@ -709,17 +540,12 @@ void testFirmwareBinExactMatch() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://exact");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://exact");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 200u);
|
EXPECT_EQ(p.getFirmwareSize(), 200u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testLargeSize() {
|
TEST(ReleaseJsonParser, LargeSize) {
|
||||||
printf("testLargeSize...\n");
|
|
||||||
|
|
||||||
// 16MB firmware (maximum flash size)
|
// 16MB firmware (maximum flash size)
|
||||||
const char* json =
|
const char* json =
|
||||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":16777216}]})";
|
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":16777216}]})";
|
||||||
@@ -727,49 +553,34 @@ void testLargeSize() {
|
|||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 16777216u);
|
EXPECT_EQ(p.getFirmwareSize(), 16777216u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testSizeZero() {
|
TEST(ReleaseJsonParser, SizeZero) {
|
||||||
printf("testSizeZero...\n");
|
|
||||||
|
|
||||||
const char* json =
|
const char* json =
|
||||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":0}]})";
|
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":0}]})";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 0u);
|
EXPECT_EQ(p.getFirmwareSize(), 0u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testMinimalValidJson() {
|
TEST(ReleaseJsonParser, MinimalValidJson) {
|
||||||
printf("testMinimalValidJson...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"tag_name":"v0","assets":[{"name":"firmware.bin","browser_download_url":"u","size":1}]})";
|
const char* json = R"({"tag_name":"v0","assets":[{"name":"firmware.bin","browser_download_url":"u","size":1}]})";
|
||||||
|
|
||||||
ReleaseJsonParser p;
|
ReleaseJsonParser p;
|
||||||
p.feed(json, strlen(json));
|
p.feed(json, strlen(json));
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag());
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware());
|
||||||
ASSERT_STREQ(p.getTagName(), "v0");
|
EXPECT_STREQ(p.getTagName(), "v0");
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "u");
|
EXPECT_STREQ(p.getFirmwareUrl(), "u");
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 1u);
|
EXPECT_EQ(p.getFirmwareSize(), 1u);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedRealisticEveryBoundary() {
|
TEST(ReleaseJsonParser, ChunkedRealisticEveryBoundary) {
|
||||||
printf("testChunkedRealisticEveryBoundary...\n");
|
|
||||||
|
|
||||||
// Two-chunk split at every byte boundary on a compact JSON
|
// Two-chunk split at every byte boundary on a compact JSON
|
||||||
const char* json =
|
const char* json =
|
||||||
R"({"tag_name":"v2.0","assets":[{"name":"firmware.bin","browser_download_url":"https://example.com/fw","size":9999}]})";
|
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 > 0) p.feed(json, split);
|
||||||
if (split < len) p.feed(json + split, len - split);
|
if (split < len) p.feed(json + split, len - split);
|
||||||
|
|
||||||
ASSERT_TRUE(p.foundTag());
|
EXPECT_TRUE(p.foundTag()) << "split=" << split;
|
||||||
ASSERT_TRUE(p.foundFirmware());
|
EXPECT_TRUE(p.foundFirmware()) << "split=" << split;
|
||||||
ASSERT_STREQ(p.getTagName(), "v2.0");
|
EXPECT_STREQ(p.getTagName(), "v2.0") << "split=" << split;
|
||||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw");
|
EXPECT_STREQ(p.getFirmwareUrl(), "https://example.com/fw") << "split=" << split;
|
||||||
ASSERT_EQ(p.getFirmwareSize(), 9999u);
|
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" "$@"
|
|
||||||
@@ -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" "$@"
|
|
||||||
@@ -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" "$@"
|
|
||||||
@@ -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" "$@"
|
|
||||||
@@ -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 <gtest/gtest.h>
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "lib/JsonParser/StreamingJsonParser.h"
|
#include "lib/JsonParser/StreamingJsonParser.h"
|
||||||
|
|
||||||
static int testsPassed = 0;
|
namespace {
|
||||||
static int testsFailed = 0;
|
|
||||||
|
|
||||||
#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 {
|
enum class EventType {
|
||||||
KEY,
|
KEY,
|
||||||
STRING,
|
STRING,
|
||||||
@@ -54,40 +30,36 @@ struct TestContext {
|
|||||||
std::vector<Event> events;
|
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_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_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_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_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, {}}); }
|
void onNull(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::NULL_VAL, {}}); }
|
||||||
static void onObjectStart(void* ctx) {
|
void onObjectStart(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_START, {}}); }
|
||||||
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, {}}); }
|
||||||
static void onObjectEnd(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_END, {}}); }
|
void onArrayEnd(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::ARRAY_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, {}}); }
|
|
||||||
|
|
||||||
static JsonCallbacks makeCallbacks(TestContext* ctx) {
|
JsonCallbacks makeCallbacks(TestContext* ctx) {
|
||||||
return {ctx, onKey, onString, onNumber, onBool, onNull, onObjectStart, onObjectEnd, onArrayStart, onArrayEnd};
|
return {ctx, onKey, onString, onNumber, onBool, onNull, onObjectStart, onObjectEnd, onArrayStart, onArrayEnd};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feed entire input at once
|
std::vector<Event> parse(const char* json) {
|
||||||
static std::vector<Event> parse(const char* json) {
|
|
||||||
TestContext ctx;
|
TestContext ctx;
|
||||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||||
parser.feed(json, strlen(json));
|
parser.feed(json, strlen(json));
|
||||||
return ctx.events;
|
return ctx.events;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feed input one byte at a time
|
std::vector<Event> parseBytewise(const char* json) {
|
||||||
static std::vector<Event> parseBytewise(const char* json) {
|
|
||||||
TestContext ctx;
|
TestContext ctx;
|
||||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||||
size_t len = strlen(json);
|
size_t len = strlen(json);
|
||||||
@@ -97,176 +69,132 @@ static std::vector<Event> parseBytewise(const char* json) {
|
|||||||
return ctx.events;
|
return ctx.events;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
} // namespace
|
||||||
// Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
void testSimpleObject() {
|
|
||||||
printf("testSimpleObject...\n");
|
|
||||||
|
|
||||||
|
TEST(StreamingJsonParser, SimpleObject) {
|
||||||
auto events = parse(R"({"key": "value", "num": 42})");
|
auto events = parse(R"({"key": "value", "num": 42})");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 6u);
|
ASSERT_EQ(events.size(), 6u);
|
||||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[0].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
EXPECT_EQ(events[1].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[1].value, "key");
|
EXPECT_EQ(events[1].value, "key");
|
||||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
EXPECT_EQ(events[2].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[2].value, "value");
|
EXPECT_EQ(events[2].value, "value");
|
||||||
ASSERT_EQ(events[3].type, EventType::KEY);
|
EXPECT_EQ(events[3].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[3].value, "num");
|
EXPECT_EQ(events[3].value, "num");
|
||||||
ASSERT_EQ(events[4].type, EventType::NUMBER);
|
EXPECT_EQ(events[4].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[4].value, "42");
|
EXPECT_EQ(events[4].value, "42");
|
||||||
ASSERT_EQ(events[5].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[5].type, EventType::OBJECT_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNestedObjects() {
|
TEST(StreamingJsonParser, NestedObjects) {
|
||||||
printf("testNestedObjects...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"a": {"b": "c"}})");
|
auto events = parse(R"({"a": {"b": "c"}})");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 7u);
|
ASSERT_EQ(events.size(), 7u);
|
||||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[0].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
EXPECT_EQ(events[1].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[1].value, "a");
|
EXPECT_EQ(events[1].value, "a");
|
||||||
ASSERT_EQ(events[2].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[2].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[3].type, EventType::KEY);
|
EXPECT_EQ(events[3].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[3].value, "b");
|
EXPECT_EQ(events[3].value, "b");
|
||||||
ASSERT_EQ(events[4].type, EventType::STRING);
|
EXPECT_EQ(events[4].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[4].value, "c");
|
EXPECT_EQ(events[4].value, "c");
|
||||||
ASSERT_EQ(events[5].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[5].type, EventType::OBJECT_END);
|
||||||
ASSERT_EQ(events[6].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[6].type, EventType::OBJECT_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testArrayOfValues() {
|
TEST(StreamingJsonParser, ArrayOfValues) {
|
||||||
printf("testArrayOfValues...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"items": [1, "two", true, false, null]})");
|
auto events = parse(R"({"items": [1, "two", true, false, null]})");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 10u);
|
ASSERT_EQ(events.size(), 10u);
|
||||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[0].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
EXPECT_EQ(events[1].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[1].value, "items");
|
EXPECT_EQ(events[1].value, "items");
|
||||||
ASSERT_EQ(events[2].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[2].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[3].type, EventType::NUMBER);
|
EXPECT_EQ(events[3].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[3].value, "1");
|
EXPECT_EQ(events[3].value, "1");
|
||||||
ASSERT_EQ(events[4].type, EventType::STRING);
|
EXPECT_EQ(events[4].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[4].value, "two");
|
EXPECT_EQ(events[4].value, "two");
|
||||||
ASSERT_EQ(events[5].type, EventType::BOOL_TRUE);
|
EXPECT_EQ(events[5].type, EventType::BOOL_TRUE);
|
||||||
ASSERT_EQ(events[6].type, EventType::BOOL_FALSE);
|
EXPECT_EQ(events[6].type, EventType::BOOL_FALSE);
|
||||||
ASSERT_EQ(events[7].type, EventType::NULL_VAL);
|
EXPECT_EQ(events[7].type, EventType::NULL_VAL);
|
||||||
ASSERT_EQ(events[8].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[8].type, EventType::ARRAY_END);
|
||||||
ASSERT_EQ(events[9].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[9].type, EventType::OBJECT_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testArrayOfObjects() {
|
TEST(StreamingJsonParser, ArrayOfObjects) {
|
||||||
printf("testArrayOfObjects...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"([{"a": 1}, {"b": 2}])");
|
auto events = parse(R"([{"a": 1}, {"b": 2}])");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 10u);
|
ASSERT_EQ(events.size(), 10u);
|
||||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[0].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[1].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[2].type, EventType::KEY);
|
EXPECT_EQ(events[2].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[2].value, "a");
|
EXPECT_EQ(events[2].value, "a");
|
||||||
ASSERT_EQ(events[3].type, EventType::NUMBER);
|
EXPECT_EQ(events[3].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[3].value, "1");
|
EXPECT_EQ(events[3].value, "1");
|
||||||
ASSERT_EQ(events[4].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[4].type, EventType::OBJECT_END);
|
||||||
ASSERT_EQ(events[5].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[5].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[6].type, EventType::KEY);
|
EXPECT_EQ(events[6].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[6].value, "b");
|
EXPECT_EQ(events[6].value, "b");
|
||||||
ASSERT_EQ(events[7].type, EventType::NUMBER);
|
EXPECT_EQ(events[7].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[7].value, "2");
|
EXPECT_EQ(events[7].value, "2");
|
||||||
ASSERT_EQ(events[8].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[8].type, EventType::OBJECT_END);
|
||||||
ASSERT_EQ(events[9].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[9].type, EventType::ARRAY_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testStringEscapes() {
|
TEST(StreamingJsonParser, StringEscapes) {
|
||||||
printf("testStringEscapes...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"esc": "a\"b\\c\/d\ne\tf"})");
|
auto events = parse(R"({"esc": "a\"b\\c\/d\ne\tf"})");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 4u);
|
ASSERT_EQ(events.size(), 4u);
|
||||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
EXPECT_EQ(events[2].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[2].value, std::string("a\"b\\c/d\ne\tf"));
|
EXPECT_EQ(events[2].value, std::string("a\"b\\c/d\ne\tf"));
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testUnicodeEscapePassthrough() {
|
TEST(StreamingJsonParser, UnicodeEscapePassthrough) {
|
||||||
printf("testUnicodeEscapePassthrough...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"u": "\u0041\u0042"})");
|
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
|
// \uXXXX passed through as literal \u followed by the hex digits
|
||||||
ASSERT_EQ(events[2].value, "\\u0041\\u0042");
|
EXPECT_EQ(events[2].value, "\\u0041\\u0042");
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNumbers() {
|
TEST(StreamingJsonParser, Numbers) {
|
||||||
printf("testNumbers...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"int": 42, "neg": -7, "flt": 3.14, "exp": 1e10, "nexp": -2.5E-3})");
|
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.size(), 12u);
|
||||||
ASSERT_EQ(events[2].value, "42");
|
EXPECT_EQ(events[2].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[4].type, EventType::NUMBER);
|
EXPECT_EQ(events[2].value, "42");
|
||||||
ASSERT_EQ(events[4].value, "-7");
|
EXPECT_EQ(events[4].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[6].type, EventType::NUMBER);
|
EXPECT_EQ(events[4].value, "-7");
|
||||||
ASSERT_EQ(events[6].value, "3.14");
|
EXPECT_EQ(events[6].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[8].type, EventType::NUMBER);
|
EXPECT_EQ(events[6].value, "3.14");
|
||||||
ASSERT_EQ(events[8].value, "1e10");
|
EXPECT_EQ(events[8].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[10].type, EventType::NUMBER);
|
EXPECT_EQ(events[8].value, "1e10");
|
||||||
ASSERT_EQ(events[10].value, "-2.5E-3");
|
EXPECT_EQ(events[10].type, EventType::NUMBER);
|
||||||
|
EXPECT_EQ(events[10].value, "-2.5E-3");
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testBooleansAndNull() {
|
TEST(StreamingJsonParser, BooleansAndNull) {
|
||||||
printf("testBooleansAndNull...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"t": true, "f": false, "n": null})");
|
auto events = parse(R"({"t": true, "f": false, "n": null})");
|
||||||
|
|
||||||
ASSERT_EQ(events[2].type, EventType::BOOL_TRUE);
|
ASSERT_EQ(events.size(), 8u);
|
||||||
ASSERT_EQ(events[4].type, EventType::BOOL_FALSE);
|
EXPECT_EQ(events[2].type, EventType::BOOL_TRUE);
|
||||||
ASSERT_EQ(events[6].type, EventType::NULL_VAL);
|
EXPECT_EQ(events[4].type, EventType::BOOL_FALSE);
|
||||||
|
EXPECT_EQ(events[6].type, EventType::NULL_VAL);
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedFeeding() {
|
TEST(StreamingJsonParser, ChunkedFeeding) {
|
||||||
printf("testChunkedFeeding...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"key": "value", "num": 42, "arr": [1, 2]})";
|
const char* json = R"({"key": "value", "num": 42, "arr": [1, 2]})";
|
||||||
auto reference = parse(json);
|
auto reference = parse(json);
|
||||||
|
|
||||||
// Feed byte-by-byte and verify identical event sequence
|
|
||||||
auto bytewise = parseBytewise(json);
|
auto bytewise = parseBytewise(json);
|
||||||
|
|
||||||
ASSERT_EQ(bytewise.size(), reference.size());
|
ASSERT_EQ(bytewise.size(), reference.size());
|
||||||
for (size_t i = 0; i < reference.size(); ++i) {
|
for (size_t i = 0; i < reference.size(); ++i) {
|
||||||
ASSERT_EQ(bytewise[i].type, reference[i].type);
|
EXPECT_EQ(bytewise[i].type, reference[i].type);
|
||||||
ASSERT_EQ(bytewise[i].value, reference[i].value);
|
EXPECT_EQ(bytewise[i].value, reference[i].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feed in chunks of varying size
|
|
||||||
for (size_t chunkSize = 2; chunkSize <= 7; ++chunkSize) {
|
for (size_t chunkSize = 2; chunkSize <= 7; ++chunkSize) {
|
||||||
TestContext ctx;
|
TestContext ctx;
|
||||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||||
@@ -277,20 +205,15 @@ void testChunkedFeeding() {
|
|||||||
parser.feed(json + offset, feedLen);
|
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) {
|
for (size_t i = 0; i < reference.size(); ++i) {
|
||||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
EXPECT_EQ(ctx.events[i].type, reference[i].type) << "chunkSize=" << chunkSize << " event=" << i;
|
||||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
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() {
|
TEST(StreamingJsonParser, EveryByteBoundary) {
|
||||||
printf("testEveryByteBoundary...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"tag_name":"v1.2.3","assets":[{"name":"firmware.bin","size":12345}]})";
|
const char* json = R"({"tag_name":"v1.2.3","assets":[{"name":"firmware.bin","size":12345}]})";
|
||||||
auto reference = parse(json);
|
auto reference = parse(json);
|
||||||
size_t len = strlen(json);
|
size_t len = strlen(json);
|
||||||
@@ -301,137 +224,94 @@ void testEveryByteBoundary() {
|
|||||||
if (split > 0) parser.feed(json, split);
|
if (split > 0) parser.feed(json, split);
|
||||||
if (split < len) parser.feed(json + split, len - 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) {
|
for (size_t i = 0; i < reference.size(); ++i) {
|
||||||
if (ctx.events[i].type != reference[i].type || ctx.events[i].value != reference[i].value) {
|
EXPECT_EQ(ctx.events[i].type, reference[i].type) << "split=" << split << " event=" << i;
|
||||||
fprintf(stderr, " FAIL at split=%zu, event %zu\n", split, i);
|
EXPECT_EQ(ctx.events[i].value, reference[i].value) << "split=" << split << " event=" << i;
|
||||||
testsFailed++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" passed (all %zu split points)\n", len + 1);
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testLargeTokenTruncation() {
|
TEST(StreamingJsonParser, LargeTokenTruncation) {
|
||||||
printf("testLargeTokenTruncation...\n");
|
|
||||||
|
|
||||||
// Build a string value that exceeds TOKEN_BUF_SIZE
|
// Build a string value that exceeds TOKEN_BUF_SIZE
|
||||||
std::string longVal(StreamingJsonParser::TOKEN_BUF_SIZE + 100, 'x');
|
std::string longVal(StreamingJsonParser::TOKEN_BUF_SIZE + 100, 'x');
|
||||||
std::string json = R"({"short": "ok", "long": ")" + longVal + R"("})";
|
std::string json = R"({"short": "ok", "long": ")" + longVal + R"("})";
|
||||||
|
|
||||||
auto events = parse(json.c_str());
|
auto events = parse(json.c_str());
|
||||||
|
|
||||||
// "short" key + "ok" value should still fire
|
ASSERT_GE(events.size(), 3u);
|
||||||
ASSERT_TRUE(events.size() >= 3);
|
EXPECT_EQ(events[1].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
EXPECT_EQ(events[1].value, "short");
|
||||||
ASSERT_EQ(events[1].value, "short");
|
EXPECT_EQ(events[2].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
EXPECT_EQ(events[2].value, "ok");
|
||||||
ASSERT_EQ(events[2].value, "ok");
|
|
||||||
|
|
||||||
// The "long" key fires, but the oversized value is silently dropped
|
|
||||||
bool foundLongKey = false;
|
bool foundLongKey = false;
|
||||||
bool foundLongValue = false;
|
bool foundLongValue = false;
|
||||||
for (auto& e : events) {
|
for (auto& e : events) {
|
||||||
if (e.type == EventType::KEY && e.value == "long") foundLongKey = true;
|
if (e.type == EventType::KEY && e.value == "long") foundLongKey = true;
|
||||||
if (e.type == EventType::STRING && e.value.size() > 500) foundLongValue = true;
|
if (e.type == EventType::STRING && e.value.size() > 500) foundLongValue = true;
|
||||||
}
|
}
|
||||||
ASSERT_TRUE(foundLongKey);
|
EXPECT_TRUE(foundLongKey);
|
||||||
ASSERT_TRUE(!foundLongValue);
|
EXPECT_FALSE(foundLongValue);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testEmptyObject() {
|
TEST(StreamingJsonParser, EmptyObject) {
|
||||||
printf("testEmptyObject...\n");
|
|
||||||
|
|
||||||
auto events = parse("{}");
|
auto events = parse("{}");
|
||||||
ASSERT_EQ(events.size(), 2u);
|
ASSERT_EQ(events.size(), 2u);
|
||||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[0].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[1].type, EventType::OBJECT_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testEmptyArray() {
|
TEST(StreamingJsonParser, EmptyArray) {
|
||||||
printf("testEmptyArray...\n");
|
|
||||||
|
|
||||||
auto events = parse("[]");
|
auto events = parse("[]");
|
||||||
ASSERT_EQ(events.size(), 2u);
|
ASSERT_EQ(events.size(), 2u);
|
||||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[0].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[1].type, EventType::ARRAY_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNestedArrays() {
|
TEST(StreamingJsonParser, NestedArrays) {
|
||||||
printf("testNestedArrays...\n");
|
|
||||||
|
|
||||||
auto events = parse("[[1, 2], [3]]");
|
auto events = parse("[[1, 2], [3]]");
|
||||||
ASSERT_EQ(events.size(), 9u);
|
ASSERT_EQ(events.size(), 9u);
|
||||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[0].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[1].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[2].type, EventType::NUMBER);
|
EXPECT_EQ(events[2].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[2].value, "1");
|
EXPECT_EQ(events[2].value, "1");
|
||||||
ASSERT_EQ(events[3].type, EventType::NUMBER);
|
EXPECT_EQ(events[3].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[3].value, "2");
|
EXPECT_EQ(events[3].value, "2");
|
||||||
ASSERT_EQ(events[4].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[4].type, EventType::ARRAY_END);
|
||||||
ASSERT_EQ(events[5].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[5].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[6].type, EventType::NUMBER);
|
EXPECT_EQ(events[6].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[6].value, "3");
|
EXPECT_EQ(events[6].value, "3");
|
||||||
ASSERT_EQ(events[7].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[7].type, EventType::ARRAY_END);
|
||||||
ASSERT_EQ(events[8].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[8].type, EventType::ARRAY_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testTopLevelArray() {
|
TEST(StreamingJsonParser, TopLevelArray) {
|
||||||
printf("testTopLevelArray...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"(["hello", 42, true, null])");
|
auto events = parse(R"(["hello", 42, true, null])");
|
||||||
ASSERT_EQ(events.size(), 6u);
|
ASSERT_EQ(events.size(), 6u);
|
||||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[0].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::STRING);
|
EXPECT_EQ(events[1].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[1].value, "hello");
|
EXPECT_EQ(events[1].value, "hello");
|
||||||
ASSERT_EQ(events[2].type, EventType::NUMBER);
|
EXPECT_EQ(events[2].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[2].value, "42");
|
EXPECT_EQ(events[2].value, "42");
|
||||||
ASSERT_EQ(events[3].type, EventType::BOOL_TRUE);
|
EXPECT_EQ(events[3].type, EventType::BOOL_TRUE);
|
||||||
ASSERT_EQ(events[4].type, EventType::NULL_VAL);
|
EXPECT_EQ(events[4].type, EventType::NULL_VAL);
|
||||||
ASSERT_EQ(events[5].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[5].type, EventType::ARRAY_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWhitespaceVariants() {
|
TEST(StreamingJsonParser, WhitespaceVariants) {
|
||||||
printf("testWhitespaceVariants...\n");
|
|
||||||
|
|
||||||
// Minified
|
|
||||||
auto minified = parse(R"({"a":1,"b":"x"})");
|
auto minified = parse(R"({"a":1,"b":"x"})");
|
||||||
|
|
||||||
// Pretty-printed
|
|
||||||
const char* pretty = "{\n \"a\": 1,\n \"b\": \"x\"\n}";
|
const char* pretty = "{\n \"a\": 1,\n \"b\": \"x\"\n}";
|
||||||
auto prettyEvents = parse(pretty);
|
auto prettyEvents = parse(pretty);
|
||||||
|
|
||||||
ASSERT_EQ(minified.size(), prettyEvents.size());
|
ASSERT_EQ(minified.size(), prettyEvents.size());
|
||||||
for (size_t i = 0; i < minified.size(); ++i) {
|
for (size_t i = 0; i < minified.size(); ++i) {
|
||||||
ASSERT_EQ(minified[i].type, prettyEvents[i].type);
|
EXPECT_EQ(minified[i].type, prettyEvents[i].type);
|
||||||
ASSERT_EQ(minified[i].value, prettyEvents[i].value);
|
EXPECT_EQ(minified[i].value, prettyEvents[i].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testResetBetweenDocuments() {
|
TEST(StreamingJsonParser, ResetBetweenDocuments) {
|
||||||
printf("testResetBetweenDocuments...\n");
|
|
||||||
|
|
||||||
TestContext ctx;
|
TestContext ctx;
|
||||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||||
|
|
||||||
@@ -445,52 +325,34 @@ void testResetBetweenDocuments() {
|
|||||||
const char* json2 = R"({"b": 2})";
|
const char* json2 = R"({"b": 2})";
|
||||||
parser.feed(json2, strlen(json2));
|
parser.feed(json2, strlen(json2));
|
||||||
ASSERT_EQ(ctx.events.size(), 4u);
|
ASSERT_EQ(ctx.events.size(), 4u);
|
||||||
ASSERT_EQ(ctx.events[1].value, "b");
|
EXPECT_EQ(ctx.events[1].value, "b");
|
||||||
ASSERT_EQ(ctx.events[2].value, "2");
|
EXPECT_EQ(ctx.events[2].value, "2");
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNumberAtEndOfInput() {
|
TEST(StreamingJsonParser, NumberAtEndOfInput) {
|
||||||
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).
|
|
||||||
auto events = parse(R"({"n": 99})");
|
auto events = parse(R"({"n": 99})");
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (auto& e : events) {
|
for (auto& e : events) {
|
||||||
if (e.type == EventType::NUMBER && e.value == "99") found = true;
|
if (e.type == EventType::NUMBER && e.value == "99") found = true;
|
||||||
}
|
}
|
||||||
ASSERT_TRUE(found);
|
EXPECT_TRUE(found);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testArrayOfStrings() {
|
TEST(StreamingJsonParser, ArrayOfStrings) {
|
||||||
printf("testArrayOfStrings...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"(["a", "b", "c"])");
|
auto events = parse(R"(["a", "b", "c"])");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 5u);
|
ASSERT_EQ(events.size(), 5u);
|
||||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[0].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::STRING);
|
EXPECT_EQ(events[1].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[1].value, "a");
|
EXPECT_EQ(events[1].value, "a");
|
||||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
EXPECT_EQ(events[2].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[2].value, "b");
|
EXPECT_EQ(events[2].value, "b");
|
||||||
ASSERT_EQ(events[3].type, EventType::STRING);
|
EXPECT_EQ(events[3].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[3].value, "c");
|
EXPECT_EQ(events[3].value, "c");
|
||||||
ASSERT_EQ(events[4].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[4].type, EventType::ARRAY_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testTruncatedInputNoCrash() {
|
TEST(StreamingJsonParser, TruncatedInputNoCrash) {
|
||||||
printf("testTruncatedInputNoCrash...\n");
|
|
||||||
|
|
||||||
// Simulates a connection drop mid-JSON. Parser must not crash.
|
|
||||||
const char* truncated[] = {
|
const char* truncated[] = {
|
||||||
R"({"key": "val)", R"({"key": )", R"({"key)", R"([1, 2, )",
|
R"({"key": "val)", R"({"key": )", R"({"key)", R"([1, 2, )",
|
||||||
R"({"a": tru)", R"({"a": fal)", R"({"a": nul)", R"({"a": "hello\)",
|
R"({"a": tru)", R"({"a": fal)", R"({"a": nul)", R"({"a": "hello\)",
|
||||||
@@ -502,49 +364,36 @@ void testTruncatedInputNoCrash() {
|
|||||||
parser.feed(json, strlen(json));
|
parser.feed(json, strlen(json));
|
||||||
// Just verify no crash; partial results are acceptable
|
// Just verify no crash; partial results are acceptable
|
||||||
}
|
}
|
||||||
|
SUCCEED();
|
||||||
printf(" passed (no crashes on %d truncated inputs)\n", 8);
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testAllEscapeSequences() {
|
TEST(StreamingJsonParser, AllEscapeSequences) {
|
||||||
printf("testAllEscapeSequences...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"e": "\b\f\n\r\t\"\\\/"})");
|
auto events = parse(R"({"e": "\b\f\n\r\t\"\\\/"})");
|
||||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
ASSERT_EQ(events.size(), 4u);
|
||||||
ASSERT_EQ(events[2].value, std::string("\b\f\n\r\t\"\\/"));
|
EXPECT_EQ(events[2].type, EventType::STRING);
|
||||||
|
EXPECT_EQ(events[2].value, std::string("\b\f\n\r\t\"\\/"));
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testObjectInArray() {
|
TEST(StreamingJsonParser, ObjectInArray) {
|
||||||
printf("testObjectInArray...\n");
|
|
||||||
|
|
||||||
// After an object closes inside an array, the next string after comma
|
// After an object closes inside an array, the next string after comma
|
||||||
// should be correctly identified as a key (inside the next object) or
|
// should be correctly identified as a key (inside the next object) or
|
||||||
// a string value (if directly in the array).
|
// a string value (if directly in the array).
|
||||||
auto events = parse(R"([{"k":"v"}, "bare"])");
|
auto events = parse(R"([{"k":"v"}, "bare"])");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 7u);
|
ASSERT_EQ(events.size(), 7u);
|
||||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
EXPECT_EQ(events[0].type, EventType::ARRAY_START);
|
||||||
ASSERT_EQ(events[1].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[1].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[2].type, EventType::KEY);
|
EXPECT_EQ(events[2].type, EventType::KEY);
|
||||||
ASSERT_EQ(events[2].value, "k");
|
EXPECT_EQ(events[2].value, "k");
|
||||||
ASSERT_EQ(events[3].type, EventType::STRING);
|
EXPECT_EQ(events[3].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[3].value, "v");
|
EXPECT_EQ(events[3].value, "v");
|
||||||
ASSERT_EQ(events[4].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[4].type, EventType::OBJECT_END);
|
||||||
ASSERT_EQ(events[5].type, EventType::STRING);
|
EXPECT_EQ(events[5].type, EventType::STRING);
|
||||||
ASSERT_EQ(events[5].value, "bare");
|
EXPECT_EQ(events[5].value, "bare");
|
||||||
ASSERT_EQ(events[6].type, EventType::ARRAY_END);
|
EXPECT_EQ(events[6].type, EventType::ARRAY_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testDeeplyNested() {
|
TEST(StreamingJsonParser, DeeplyNested) {
|
||||||
printf("testDeeplyNested...\n");
|
|
||||||
|
|
||||||
// 20 levels of nesting (well within MAX_NESTING=32)
|
// 20 levels of nesting (well within MAX_NESTING=32)
|
||||||
std::string json;
|
std::string json;
|
||||||
for (int i = 0; i < 20; ++i) json += R"({"d":)";
|
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
|
// 20 OBJECT_START + 20 KEY + 1 NUMBER + 20 OBJECT_END = 61
|
||||||
ASSERT_EQ(events.size(), 61u);
|
ASSERT_EQ(events.size(), 61u);
|
||||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
EXPECT_EQ(events[0].type, EventType::OBJECT_START);
|
||||||
ASSERT_EQ(events[40].type, EventType::NUMBER);
|
EXPECT_EQ(events[40].type, EventType::NUMBER);
|
||||||
ASSERT_EQ(events[40].value, "0");
|
EXPECT_EQ(events[40].value, "0");
|
||||||
ASSERT_EQ(events[60].type, EventType::OBJECT_END);
|
EXPECT_EQ(events[60].type, EventType::OBJECT_END);
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNestingOverflow() {
|
TEST(StreamingJsonParser, NestingOverflow) {
|
||||||
printf("testNestingOverflow...\n");
|
|
||||||
|
|
||||||
// Exceed MAX_NESTING -- parser should set error flag, not crash
|
// Exceed MAX_NESTING -- parser should set error flag, not crash
|
||||||
std::string json;
|
std::string json;
|
||||||
for (size_t i = 0; i < StreamingJsonParser::MAX_NESTING + 5; ++i) json += "[";
|
for (size_t i = 0; i < StreamingJsonParser::MAX_NESTING + 5; ++i) json += "[";
|
||||||
@@ -575,47 +419,32 @@ void testNestingOverflow() {
|
|||||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||||
parser.feed(json.c_str(), json.size());
|
parser.feed(json.c_str(), json.size());
|
||||||
|
|
||||||
ASSERT_TRUE(parser.hasError());
|
EXPECT_TRUE(parser.hasError());
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNumberZero() {
|
TEST(StreamingJsonParser, NumberZero) {
|
||||||
printf("testNumberZero...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"z": 0})");
|
auto events = parse(R"({"z": 0})");
|
||||||
ASSERT_EQ(events[2].type, EventType::NUMBER);
|
ASSERT_EQ(events.size(), 4u);
|
||||||
ASSERT_EQ(events[2].value, "0");
|
EXPECT_EQ(events[2].type, EventType::NUMBER);
|
||||||
|
EXPECT_EQ(events[2].value, "0");
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testMultipleValuesInObject() {
|
TEST(StreamingJsonParser, MultipleValuesInObject) {
|
||||||
printf("testMultipleValuesInObject...\n");
|
|
||||||
|
|
||||||
auto events = parse(R"({"a": "x", "b": "y", "c": "z"})");
|
auto events = parse(R"({"a": "x", "b": "y", "c": "z"})");
|
||||||
|
|
||||||
ASSERT_EQ(events.size(), 8u);
|
ASSERT_EQ(events.size(), 8u);
|
||||||
ASSERT_EQ(events[1].value, "a");
|
EXPECT_EQ(events[1].value, "a");
|
||||||
ASSERT_EQ(events[2].value, "x");
|
EXPECT_EQ(events[2].value, "x");
|
||||||
ASSERT_EQ(events[3].value, "b");
|
EXPECT_EQ(events[3].value, "b");
|
||||||
ASSERT_EQ(events[4].value, "y");
|
EXPECT_EQ(events[4].value, "y");
|
||||||
ASSERT_EQ(events[5].value, "c");
|
EXPECT_EQ(events[5].value, "c");
|
||||||
ASSERT_EQ(events[6].value, "z");
|
EXPECT_EQ(events[6].value, "z");
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedSplitInsideString() {
|
TEST(StreamingJsonParser, ChunkedSplitInsideString) {
|
||||||
printf("testChunkedSplitInsideString...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"key": "hello world"})";
|
const char* json = R"({"key": "hello world"})";
|
||||||
auto reference = parse(json);
|
auto reference = parse(json);
|
||||||
|
|
||||||
// Split right in the middle of "hello world"
|
|
||||||
size_t splitAt = 14; // inside the string value
|
size_t splitAt = 14; // inside the string value
|
||||||
TestContext ctx;
|
TestContext ctx;
|
||||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||||
@@ -624,23 +453,17 @@ void testChunkedSplitInsideString() {
|
|||||||
|
|
||||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||||
for (size_t i = 0; i < reference.size(); ++i) {
|
for (size_t i = 0; i < reference.size(); ++i) {
|
||||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
EXPECT_EQ(ctx.events[i].type, reference[i].type);
|
||||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
EXPECT_EQ(ctx.events[i].value, reference[i].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedSplitInsideEscape() {
|
TEST(StreamingJsonParser, ChunkedSplitInsideEscape) {
|
||||||
printf("testChunkedSplitInsideEscape...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"k": "a\"b"})";
|
const char* json = R"({"k": "a\"b"})";
|
||||||
auto reference = parse(json);
|
auto reference = parse(json);
|
||||||
|
|
||||||
// Find the backslash position and split right after it
|
|
||||||
const char* bs = strchr(json + 7, '\\');
|
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
|
size_t splitAt = static_cast<size_t>(bs - json) + 1; // after the backslash
|
||||||
|
|
||||||
TestContext ctx;
|
TestContext ctx;
|
||||||
@@ -650,22 +473,16 @@ void testChunkedSplitInsideEscape() {
|
|||||||
|
|
||||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||||
for (size_t i = 0; i < reference.size(); ++i) {
|
for (size_t i = 0; i < reference.size(); ++i) {
|
||||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
EXPECT_EQ(ctx.events[i].type, reference[i].type);
|
||||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
EXPECT_EQ(ctx.events[i].value, reference[i].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChunkedSplitInsideLiteral() {
|
TEST(StreamingJsonParser, ChunkedSplitInsideLiteral) {
|
||||||
printf("testChunkedSplitInsideLiteral...\n");
|
|
||||||
|
|
||||||
const char* json = R"({"a": true, "b": false, "c": null})";
|
const char* json = R"({"a": true, "b": false, "c": null})";
|
||||||
auto reference = parse(json);
|
auto reference = parse(json);
|
||||||
|
|
||||||
// Split inside "true" (at "tr|ue")
|
size_t splitAt = 7; // inside "true"
|
||||||
size_t splitAt = 7;
|
|
||||||
TestContext ctx;
|
TestContext ctx;
|
||||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||||
parser.feed(json, splitAt);
|
parser.feed(json, splitAt);
|
||||||
@@ -673,65 +490,17 @@ void testChunkedSplitInsideLiteral() {
|
|||||||
|
|
||||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||||
for (size_t i = 0; i < reference.size(); ++i) {
|
for (size_t i = 0; i < reference.size(); ++i) {
|
||||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
EXPECT_EQ(ctx.events[i].type, reference[i].type);
|
||||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
EXPECT_EQ(ctx.events[i].value, reference[i].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" passed\n");
|
|
||||||
PASS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNullCallbacksNoCrash() {
|
TEST(StreamingJsonParser, NullCallbacksNoCrash) {
|
||||||
printf("testNullCallbacksNoCrash...\n");
|
|
||||||
|
|
||||||
JsonCallbacks nullCbs = {};
|
JsonCallbacks nullCbs = {};
|
||||||
nullCbs.ctx = nullptr;
|
nullCbs.ctx = nullptr;
|
||||||
StreamingJsonParser parser(nullCbs);
|
StreamingJsonParser parser(nullCbs);
|
||||||
|
|
||||||
const char* json = R"({"key": "value", "num": 42, "b": true, "n": null, "a": [1]})";
|
const char* json = R"({"key": "value", "num": 42, "b": true, "n": null, "a": [1]})";
|
||||||
parser.feed(json, strlen(json));
|
parser.feed(json, strlen(json));
|
||||||
ASSERT_TRUE(!parser.hasError());
|
EXPECT_FALSE(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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user