diff --git a/.gitea/workflows/bindings.yml b/.gitea/workflows/bindings.yml index 0d02bac..b70b6ca 100644 --- a/.gitea/workflows/bindings.yml +++ b/.gitea/workflows/bindings.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bb2c68..be20451 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 42db8c6..4d5fbf4 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/include/ikvxx/ikvxx.hpp b/include/ikvxx/ikvxx.hpp index db8ebf8..8a0ea7c 100644 --- a/include/ikvxx/ikvxx.hpp +++ b/include/ikvxx/ikvxx.hpp @@ -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 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: diff --git a/tests/run.bat b/tests/run.bat index 43f076e..d858049 100644 --- a/tests/run.bat +++ b/tests/run.bat @@ -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 diff --git a/tests/value_tests.cpp b/tests/value_tests.cpp index 9ad8656..0fe27d5 100644 --- a/tests/value_tests.cpp +++ b/tests/value_tests.cpp @@ -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 ]\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; }