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: on:
push: push:
pull_request: pull_request:
jobs: jobs:
test: cmake-build:
name: ${{ matrix.compiler }} / ${{ matrix.build_type }}
runs-on: ubuntu-latest 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: steps:
- name: Checkout with submodules - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- name: Configure - name: Install build tools
env: run: |
CC: ${{ matrix.cc }} if ! command -v c++ >/dev/null || ! command -v cmake >/dev/null || ! command -v ninja >/dev/null; then
CXX: ${{ matrix.cxx }} apt-get update
run: >- DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential cmake ninja-build
cmake -S . -B build fi
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DIKVXX_BUILD_TESTS=ON
-DIKVXX_INSTALL=ON
- name: Build - name: Configure CMake build
run: cmake --build build --parallel 2 run: |
cmake -S . -B build -G Ninja -DIKVXX_BUILD_TESTS=OFF
- name: Test bindings - name: Build with CMake
run: ctest --test-dir build --build-config ${{ matrix.build_type }} --output-on-failure run: |
cmake --build build
- name: Verify installation unit-tests:
run: cmake --install build --prefix "${{ runner.temp }}/ikvxx-install" 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_BUILD_TESTS "Build iKvxx unit tests" ON)
option(IKVXX_INSTALL "Install iKvxx" 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_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) set(IKV_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/iKv) add_subdirectory(third_party/iKv)
set_target_properties(ikv PROPERTIES set_target_properties(ikv PROPERTIES
@@ -27,11 +31,24 @@ else()
endif() endif()
if(IKVXX_BUILD_TESTS) if(IKVXX_BUILD_TESTS)
include(CTest)
add_executable(ikvxx_tests tests/value_tests.cpp) add_executable(ikvxx_tests tests/value_tests.cpp)
target_link_libraries(ikvxx_tests PRIVATE ikvxx::ikvxx) target_link_libraries(ikvxx_tests PRIVATE ikvxx::ikvxx)
target_compile_features(ikvxx_tests PRIVATE cxx_std_17) 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() endif()
if(IKVXX_INSTALL) if(IKVXX_INSTALL)

View File

@@ -243,10 +243,12 @@ cmake --build build
ctest --test-dir build --output-on-failure ctest --test-dir build --output-on-failure
``` ```
The test suite covers construction, every value type, object and array access, CTest runs every iKvxx test group separately and also includes the complete upstream
assignment and append overloads, conversion failures, bounds checks, text and binary iKv suite. The binding tests cover construction, every value type, object and array
I/O, format versions, refresh behavior, and shared ownership. Gitea Actions runs the access, assignment and append overloads, conversion failures, bounds checks, text and
suite on Ubuntu with GCC and Clang in both Debug and Release configurations. 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: 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 { namespace ikv {
/// Runtime value types exposed by iKv.
enum ValueType { enum ValueType {
nullValue = 0, nullValue = 0,
stringValue, stringValue,
@@ -21,26 +22,44 @@ enum ValueType {
arrayValue arrayValue
}; };
/// On-disk iKv format versions supported for explicit writes.
enum class Version : unsigned { v1 = 1, v2 = 2 }; 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 { class Error : public std::runtime_error {
public: public:
using std::runtime_error::runtime_error; 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 { class Value {
public: public:
using ArrayIndex = std::uint32_t; 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"); explicit Value(ValueType type = objectValue, std::string root_name = "root");
Value(const Value&) = default; Value(const Value&) = default;
/// Structural assignment is unavailable because iKv cannot clone arbitrary subtrees.
Value& operator=(const Value&) = delete; Value& operator=(const Value&) = delete;
/// Parses auto-detected iKv1 or iKv2 text.
static Value parse(const std::string& text); static Value parse(const std::string& text);
/// Loads an auto-detected iKv1 or iKv2 text file.
static Value load(const std::string& path); 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); static Value fromBinary(const void* data, std::size_t size);
/// Loads an auto-detected binary iKv file.
static Value loadBinary(const std::string& path); static Value loadBinary(const std::string& path);
/// Returns the node's runtime type, or nullValue for a missing member.
ValueType type() const noexcept; ValueType type() const noexcept;
bool isNull() const noexcept; bool isNull() const noexcept;
bool isString() const noexcept; bool isString() const noexcept;
@@ -54,6 +73,8 @@ public:
std::string name() const; std::string name() const;
bool isMember(const std::string& key) 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 std::string& key);
Value operator[](const char* key); Value operator[](const char* key);
Value operator[](const std::string& key) const; Value operator[](const std::string& key) const;
@@ -62,8 +83,11 @@ public:
Value operator[](int index); Value operator[](int index);
Value operator[](ArrayIndex index) const; Value operator[](ArrayIndex index) const;
Value operator[](int 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; 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 std::string& value);
Value& operator=(const char* value); Value& operator=(const char* value);
Value& operator=(std::int64_t value); Value& operator=(std::int64_t value);
@@ -71,8 +95,13 @@ public:
Value& operator=(double value); Value& operator=(double value);
Value& operator=(bool value); Value& operator=(bool value);
/// Adds or replaces an object member and returns a handle to it.
Value makeObject(const std::string& key); 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); 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 std::string& value);
Value append(const char* value); Value append(const char* value);
Value append(std::int64_t value); Value append(std::int64_t value);
@@ -87,9 +116,13 @@ public:
double asDouble() const; double asDouble() const;
bool asBool() 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 write(const std::string& path, Version version = Version::v2) const;
void writeBinary(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; 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); void refresh(const std::string& path);
private: private:

View File

@@ -36,6 +36,12 @@ if errorlevel 1 (
) )
echo Running iKvxx tests... 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%" ^ ctest --test-dir "%BUILD_DIR%" ^
--build-config "%CONFIG%" ^ --build-config "%CONFIG%" ^
--output-on-failure --output-on-failure

View File

@@ -368,14 +368,40 @@ void testRefreshAndOwnership() {
} // namespace } // namespace
int main() { int main(int argc, char** argv) {
testConstructionAndTypeInspection(); struct TestCase {
testAssignmentsAndConversions(); const char* name;
testObjectsMembersAndLookupOverloads(); void (*function)();
testArraysAndIndexOverloads(); };
testTextParsingAndFiles();
testBinaryMemoryAndFiles(); const TestCase tests[] = {
testRefreshAndOwnership(); {"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"; if (failures != 0) std::cerr << failures << " test(s) failed\n";
return failures == 0 ? 0 : 1; return failures == 0 ? 0 : 1;
} }