feat(testing): expose complete iKvxx test suite

This commit is contained in:
2026-06-17 19:07:51 -05:00
parent e651ae1575
commit 3628fd1c7f
6 changed files with 138 additions and 55 deletions

View File

@@ -1,56 +1,55 @@
name: C++ bindings
name: Build
on:
push:
pull_request:
jobs:
test:
name: ${{ matrix.compiler }} / ${{ matrix.build_type }}
cmake-build:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- compiler: gcc
cc: gcc
cxx: g++
build_type: Debug
- compiler: gcc
cc: gcc
cxx: g++
build_type: Release
- compiler: clang
cc: clang
cxx: clang++
build_type: Debug
- compiler: clang
cc: clang
cxx: clang++
build_type: Release
steps:
- name: Checkout with submodules
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Configure
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
run: >-
cmake -S . -B build
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DIKVXX_BUILD_TESTS=ON
-DIKVXX_INSTALL=ON
- name: Install build tools
run: |
if ! command -v c++ >/dev/null || ! command -v cmake >/dev/null || ! command -v ninja >/dev/null; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential cmake ninja-build
fi
- name: Build
run: cmake --build build --parallel 2
- name: Configure CMake build
run: |
cmake -S . -B build -G Ninja -DIKVXX_BUILD_TESTS=OFF
- name: Test bindings
run: ctest --test-dir build --build-config ${{ matrix.build_type }} --output-on-failure
- name: Build with CMake
run: |
cmake --build build
- name: Verify installation
run: cmake --install build --prefix "${{ runner.temp }}/ikvxx-install"
unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install build tools
run: |
if ! command -v c++ >/dev/null || ! command -v cmake >/dev/null || ! command -v ninja >/dev/null; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential cmake ninja-build
fi
- name: Configure unit tests
run: |
cmake -S . -B build-tests -G Ninja -DIKVXX_BUILD_TESTS=ON -DIKVXX_INSTALL=OFF
- name: Run unit tests
run: |
cmake --build build-tests
ctest --test-dir build-tests --output-on-failure

View File

@@ -5,8 +5,12 @@ project(ikvxx VERSION 0.1.0 LANGUAGES C CXX)
option(IKVXX_BUILD_TESTS "Build iKvxx unit tests" ON)
option(IKVXX_INSTALL "Install iKvxx" ON)
if(IKVXX_BUILD_TESTS)
include(CTest)
endif()
set(IKV_BUILD_DEMOS OFF CACHE BOOL "" FORCE)
set(IKV_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(IKV_BUILD_TESTS ${IKVXX_BUILD_TESTS} CACHE BOOL "" FORCE)
set(IKV_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/iKv)
set_target_properties(ikv PROPERTIES
@@ -27,11 +31,24 @@ else()
endif()
if(IKVXX_BUILD_TESTS)
include(CTest)
add_executable(ikvxx_tests tests/value_tests.cpp)
target_link_libraries(ikvxx_tests PRIVATE ikvxx::ikvxx)
target_compile_features(ikvxx_tests PRIVATE cxx_std_17)
add_test(NAME ikvxx.value COMMAND ikvxx_tests)
set(IKVXX_TEST_CASES
construction_and_type_inspection
assignments_and_conversions
objects_members_and_lookup_overloads
arrays_and_index_overloads
text_parsing_and_files
binary_memory_and_files
refresh_and_ownership)
foreach(IKVXX_TEST_CASE IN LISTS IKVXX_TEST_CASES)
add_test(
NAME "ikvxx.${IKVXX_TEST_CASE}"
COMMAND ikvxx_tests --case "${IKVXX_TEST_CASE}")
endforeach()
endif()
if(IKVXX_INSTALL)

View File

@@ -243,10 +243,12 @@ cmake --build build
ctest --test-dir build --output-on-failure
```
The test suite covers construction, every value type, object and array access,
assignment and append overloads, conversion failures, bounds checks, text and binary
I/O, format versions, refresh behavior, and shared ownership. Gitea Actions runs the
suite on Ubuntu with GCC and Clang in both Debug and Release configurations.
CTest runs every iKvxx test group separately and also includes the complete upstream
iKv suite. The binding tests cover construction, every value type, object and array
access, assignment and append overloads, conversion failures, bounds checks, text and
binary I/O, format versions, refresh behavior, and shared ownership. The Gitea Actions
workflow follows iKv's Ubuntu build-and-test structure, with one CMake build job and
one job that runs the complete test suite.
On Windows, the complete configure, build, and test sequence can be run with:

View File

@@ -11,6 +11,7 @@ struct ikv_node_t;
namespace ikv {
/// Runtime value types exposed by iKv.
enum ValueType {
nullValue = 0,
stringValue,
@@ -21,26 +22,44 @@ enum ValueType {
arrayValue
};
/// On-disk iKv format versions supported for explicit writes.
enum class Version : unsigned { v1 = 1, v2 = 2 };
/// Error raised for invalid types, unsupported operations, parse failures,
/// and I/O failures.
class Error : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
/// RAII handle to an iKv root or a node owned by an iKv tree.
///
/// Copies share ownership of the same tree. Child handles remain valid while their
/// node exists, but should be reacquired after replacing that node or refreshing the root.
class Value {
public:
using ArrayIndex = std::uint32_t;
/// Creates an object or array root. Scalar roots are not supported by iKv.
explicit Value(ValueType type = objectValue, std::string root_name = "root");
Value(const Value&) = default;
/// Structural assignment is unavailable because iKv cannot clone arbitrary subtrees.
Value& operator=(const Value&) = delete;
/// Parses auto-detected iKv1 or iKv2 text.
static Value parse(const std::string& text);
/// Loads an auto-detected iKv1 or iKv2 text file.
static Value load(const std::string& path);
/// Parses an iKv binary buffer. The resulting tree owns any required data.
static Value fromBinary(const void* data, std::size_t size);
/// Loads an auto-detected binary iKv file.
static Value loadBinary(const std::string& path);
/// Returns the node's runtime type, or nullValue for a missing member.
ValueType type() const noexcept;
bool isNull() const noexcept;
bool isString() const noexcept;
@@ -54,6 +73,8 @@ public:
std::string name() const;
bool isMember(const std::string& key) const;
/// Looks up an object member. A missing non-const member returns an assignable null Value.
Value operator[](const std::string& key);
Value operator[](const char* key);
Value operator[](const std::string& key) const;
@@ -62,8 +83,11 @@ public:
Value operator[](int index);
Value operator[](ArrayIndex index) const;
Value operator[](int index) const;
/// Returns an object member or a shared copy of fallback when the key is absent.
Value get(const std::string& key, const Value& fallback) const;
/// Assigns a scalar to an object member returned by operator[].
Value& operator=(const std::string& value);
Value& operator=(const char* value);
Value& operator=(std::int64_t value);
@@ -71,8 +95,13 @@ public:
Value& operator=(double value);
Value& operator=(bool value);
/// Adds or replaces an object member and returns a handle to it.
Value makeObject(const std::string& key);
/// Adds an array member. nullValue creates an untyped array.
Value makeArray(const std::string& key, ValueType element_type = nullValue);
/// Appends to an array. Typed arrays reject incompatible values with Error.
Value append(const std::string& value);
Value append(const char* value);
Value append(std::int64_t value);
@@ -87,9 +116,13 @@ public:
double asDouble() const;
bool asBool() const;
/// Writes text or binary output, using iKv2 unless a version is specified.
void write(const std::string& path, Version version = Version::v2) const;
void writeBinary(const std::string& path, Version version = Version::v2) const;
std::vector<std::uint8_t> toBinary(Version version = Version::v2) const;
/// Replaces this root in place from a text or binary path while preserving shared handles.
/// Only mutable root Values can be refreshed; failure leaves the existing tree unchanged.
void refresh(const std::string& path);
private:

View File

@@ -36,6 +36,12 @@ if errorlevel 1 (
)
echo Running iKvxx tests...
ctest --test-dir "%BUILD_DIR%" --build-config "%CONFIG%" --show-only
if errorlevel 1 (
echo Error: Could not enumerate tests.
exit /b 1
)
ctest --test-dir "%BUILD_DIR%" ^
--build-config "%CONFIG%" ^
--output-on-failure

View File

@@ -368,14 +368,40 @@ void testRefreshAndOwnership() {
} // namespace
int main() {
testConstructionAndTypeInspection();
testAssignmentsAndConversions();
testObjectsMembersAndLookupOverloads();
testArraysAndIndexOverloads();
testTextParsingAndFiles();
testBinaryMemoryAndFiles();
testRefreshAndOwnership();
int main(int argc, char** argv) {
struct TestCase {
const char* name;
void (*function)();
};
const TestCase tests[] = {
{"construction_and_type_inspection", testConstructionAndTypeInspection},
{"assignments_and_conversions", testAssignmentsAndConversions},
{"objects_members_and_lookup_overloads", testObjectsMembersAndLookupOverloads},
{"arrays_and_index_overloads", testArraysAndIndexOverloads},
{"text_parsing_and_files", testTextParsingAndFiles},
{"binary_memory_and_files", testBinaryMemoryAndFiles},
{"refresh_and_ownership", testRefreshAndOwnership}
};
if (argc == 3 && std::string(argv[1]) == "--case") {
for (const auto& test : tests) {
if (argv[2] == std::string(test.name)) {
test.function();
if (failures != 0) std::cerr << failures << " test(s) failed\n";
return failures == 0 ? 0 : 1;
}
}
std::cerr << "unknown test case: " << argv[2] << '\n';
return 2;
}
if (argc != 1) {
std::cerr << "usage: ikvxx_tests [--case <name>]\n";
return 2;
}
for (const auto& test : tests) test.function();
if (failures != 0) std::cerr << failures << " test(s) failed\n";
return failures == 0 ? 0 : 1;
}