diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..e3f373e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,24 @@ +name: C++ CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build-and-test: + name: C++ - ${{ github.event_name }} + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + # 1. Check out your code + - uses: actions/checkout@v4 + # # 2. Install dependencies (if using vcpkg, Conan, etc.) + # - name: Install dependencies + # run: sudo apt update && sudo apt install -y cmake g++ make + - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + - run: cmake --build build --parallel + - run: ctest --test-dir build --output-on-failure --verbose --no-compress-output diff --git a/CMakeLists.txt b/CMakeLists.txt index 9baf83c..aa850c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,72 +1,91 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.16) project(YAMLWrapper) set(CMAKE_CXX_STANDARD 17) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -if(APPLE) - # Check for package managers - execute_process(COMMAND which brew OUTPUT_VARIABLE HOMEBREW_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND which port OUTPUT_VARIABLE MACPORTS_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND which conda OUTPUT_VARIABLE CONDA_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) +enable_testing() +include(FetchContent) + +FetchContent_Declare( + yaml-cpp + GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git + GIT_TAG 0.8.0 +) +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.5.2 +) + +FetchContent_MakeAvailable(yaml-cpp) +FetchContent_MakeAvailable(Catch2) +add_subdirectory(tests) + +file(COPY ${CMAKE_SOURCE_DIR}/lattice_files/ + DESTINATION ${CMAKE_BINARY_DIR}/lattice_files/) + +# if(APPLE) +# # Check for package managers +# execute_process(COMMAND which brew OUTPUT_VARIABLE HOMEBREW_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) +# execute_process(COMMAND which port OUTPUT_VARIABLE MACPORTS_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) +# execute_process(COMMAND which conda OUTPUT_VARIABLE CONDA_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) - # Error if both Homebrew and MacPorts exist - if(HOMEBREW_EXISTS AND MACPORTS_EXISTS) - message(FATAL_ERROR - "Both Homebrew and MacPorts detected. This can cause conflicts.\n" - "Please use only one package manager:\n" - " - Homebrew found at: ${HOMEBREW_EXISTS}\n" - " - MacPorts found at: ${MACPORTS_EXISTS}\n" - "Consider uninstalling one to avoid library conflicts." - ) - endif() +# # Error if both Homebrew and MacPorts exist +# if(HOMEBREW_EXISTS AND MACPORTS_EXISTS) +# message(FATAL_ERROR +# "Both Homebrew and MacPorts detected. This can cause conflicts.\n" +# "Please use only one package manager:\n" +# " - Homebrew found at: ${HOMEBREW_EXISTS}\n" +# " - MacPorts found at: ${MACPORTS_EXISTS}\n" +# "Consider uninstalling one to avoid library conflicts." +# ) +# endif() - # Set RPATH based on which package manager is found - set(BASE_RPATH "@executable_path/../lib;@loader_path") +# # Set RPATH based on which package manager is found +# set(BASE_RPATH "@executable_path/../lib;@loader_path") - if(HOMEBREW_EXISTS) - message(STATUS "Using Homebrew package manager") - # Check if Apple Silicon or Intel - execute_process( - COMMAND uname -m - OUTPUT_VARIABLE ARCH - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(ARCH STREQUAL "arm64") - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/homebrew/lib") - list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") - else() - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") - list(APPEND CMAKE_PREFIX_PATH "/usr/local") - endif() - elseif(MACPORTS_EXISTS) - message(STATUS "Using MacPorts package manager") - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/local/lib") - list(APPEND CMAKE_PREFIX_PATH "/opt/local") - elseif(CONDA_EXISTS) - message(STATUS "Using Conda package manager") - # Get conda prefix - execute_process( - COMMAND conda info --base - OUTPUT_VARIABLE CONDA_PREFIX - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};${CONDA_PREFIX}/lib") - list(APPEND CMAKE_PREFIX_PATH "${CONDA_PREFIX}") - else() - message(STATUS "No package manager detected, using default paths") - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") - endif() +# if(HOMEBREW_EXISTS) +# message(STATUS "Using Homebrew package manager") +# # Check if Apple Silicon or Intel +# execute_process( +# COMMAND uname -m +# OUTPUT_VARIABLE ARCH +# OUTPUT_STRIP_TRAILING_WHITESPACE +# ) +# if(ARCH STREQUAL "arm64") +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/homebrew/lib") +# list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") +# else() +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") +# list(APPEND CMAKE_PREFIX_PATH "/usr/local") +# endif() +# elseif(MACPORTS_EXISTS) +# message(STATUS "Using MacPorts package manager") +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/local/lib") +# list(APPEND CMAKE_PREFIX_PATH "/opt/local") +# elseif(CONDA_EXISTS) +# message(STATUS "Using Conda package manager") +# # Get conda prefix +# execute_process( +# COMMAND conda info --base +# OUTPUT_VARIABLE CONDA_PREFIX +# OUTPUT_STRIP_TRAILING_WHITESPACE +# ) +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};${CONDA_PREFIX}/lib") +# list(APPEND CMAKE_PREFIX_PATH "${CONDA_PREFIX}") +# else() +# message(STATUS "No package manager detected, using default paths") +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") +# endif() - set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) - message(STATUS "RPATH set to: ${CMAKE_INSTALL_RPATH}") -endif() - -find_package(yaml-cpp REQUIRED) +# set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +# message(STATUS "RPATH set to: ${CMAKE_INSTALL_RPATH}") +# endif() add_library(yaml_c_wrapper SHARED src/yaml_c_wrapper.cpp) target_link_libraries(yaml_c_wrapper yaml-cpp) -add_executable(yaml_reader src/yaml_reader.cpp) -target_link_libraries(yaml_reader yaml_c_wrapper) \ No newline at end of file +add_executable(yaml_reader examples/yaml_reader.cpp) +target_link_libraries(yaml_reader yaml_c_wrapper) diff --git a/README.md b/README.md index bbf3f24..c9d2365 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Introduction yaml_c_wrapper.cpp wraps YAML::Node into C objects so they can be part of a shared object library to interface with other languages - + -Next, in pals-cpp, run +## Usage +In pals-cpp, run -mkdir build && cd build -cmake .. -DYAML_BUILD_SHARED_LIBS=ON -make - -## Example -See yaml_reader.cpp for an example of how to use the library to read a lattice file, -perform a basic manipulation, and write to another lattice file. -Navigate to the build directly and run -./yaml_reader \ No newline at end of file +cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -S . -B build +cmake --build build + +This builds libyaml_c_wrapper.dylib, a shared object library that can be used +by other languages. + +It also builds an executable using yaml_reader.cpp containing examples for how +to use the library to read lattice files, perform basic manipulations, and write +to other lattice files. To see the output, navigate to the build directory and run +./yaml_reader + +It will also build the tests. + +## Testing +In the root pals-cpp directory, run +ctest --test-dir build --output-on-failure + +To run a specific test, run +ctest --test-dir build -R "Test Name" + +## Issue +yaml-cpp's cmake only requires cmake version 3.4, which is deprecated. Warnings must +be suppressed to run properly \ No newline at end of file diff --git a/examples/yaml_reader.cpp b/examples/yaml_reader.cpp new file mode 100644 index 0000000..1915b6b --- /dev/null +++ b/examples/yaml_reader.cpp @@ -0,0 +1,38 @@ +#include "../src/yaml_c_wrapper.h" +#include + +int main() { + // reading a lattice from a yaml file + YAMLNodeHandle handle = yaml_parse_file("../lattice_files/ex.yaml"); + // printing to terminal + std::cout << yaml_to_string(handle) << std::endl << std::endl; + + // type checking + std::cout << (yaml_is_sequence(handle)) << "\n"; + + // accessing sequence + YAMLNodeHandle node = yaml_get_index(handle, 0); + std::cout << "the first element is: \n" << yaml_to_string(node) << "\n"; + + // accessing map + std::cout << "\nthe value at key 'thingB' is: " << yaml_to_string(yaml_get_key(node, "thingB")) << "\n"; + + // creating a new node that's a map + YAMLNodeHandle map = yaml_create_map(); + yaml_set_int(map, "first", 1); + + // creating a new node that's a sequence + YAMLNodeHandle sequence = yaml_create_sequence(); + yaml_push_string(sequence, "magnet1"); + yaml_push_string(sequence, ""); + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_string(scalar, "magnet2"); + yaml_set_at_index(sequence, 1, scalar); + + // adding new nodes to lattice + yaml_push_node(handle, map); + yaml_push_node(handle, sequence); + + // writing modified lattice file to expand.yaml + yaml_write_file(handle, "../lattice_files/expand.yaml"); +} \ No newline at end of file diff --git a/lattice_files/ex.yaml b/lattice_files/ex.yaml index e29fa28..a9e9898 100644 --- a/lattice_files/ex.yaml +++ b/lattice_files/ex.yaml @@ -14,5 +14,4 @@ length: 1.03 direction: -1 - a_subline: # Item a_subline is repeated three times - repeat: 3 - hi: 2 \ No newline at end of file + repeat: 3 \ No newline at end of file diff --git a/src/yaml_c_wrapper.cpp b/src/yaml_c_wrapper.cpp index a1569f5..6fa9c15 100644 --- a/src/yaml_c_wrapper.cpp +++ b/src/yaml_c_wrapper.cpp @@ -125,9 +125,6 @@ extern "C" { return static_cast(handle)->size(); } - // === ITERATION HELPERS === - - // For iterating over maps - get all keys char** yaml_get_keys(YAMLNodeHandle handle, int* out_count) { auto node = static_cast(handle); if (!node->IsMap()) { @@ -149,20 +146,6 @@ extern "C" { return result; } - void yaml_free_keys(char** keys, int count) { - for (int i = 0; i < count; i++) { - delete[] keys[i]; - } - delete[] keys; - } - - // Set node at index for sequences - void yaml_set_at_index(YAMLNodeHandle handle, int index, YAMLNodeHandle value) { - auto node = static_cast(handle); - auto val_node = static_cast(value); - (*node)[index] = *val_node; - } - // === CONVERT TO C TYPES (caller must free returned strings) === char* yaml_as_string(YAMLNodeHandle handle) { try { @@ -225,6 +208,37 @@ extern "C" { auto val_node = static_cast(value); (*node)[key] = *val_node; } + + void yaml_set_scalar_string(YAMLNodeHandle handle, const char* value) { + if (handle == nullptr || value == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + void yaml_set_scalar_int(YAMLNodeHandle handle, int value) { + if (handle == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + void yaml_set_scalar_float(YAMLNodeHandle handle, double value) { + if (handle == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + void yaml_set_scalar_bool(YAMLNodeHandle handle, bool value) { + if (handle == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + // Set node at index for sequences + void yaml_set_at_index(YAMLNodeHandle handle, int index, YAMLNodeHandle value) { + auto node = static_cast(handle); + auto val_node = static_cast(value); + (*node)[index] = *val_node; + } void yaml_push_string(YAMLNodeHandle handle, const char* value) { auto node = static_cast(handle); @@ -395,6 +409,14 @@ extern "C" { void yaml_free_string(char* str) { delete[] str; } + + void yaml_free_keys(char** keys, int count) { + for (int i = 0; i < count; i++) { + delete[] keys[i]; + } + delete[] keys; + } + YAMLNodeHandle yaml_clone(YAMLNodeHandle handle) { return new YAML::Node(YAML::Clone(*static_cast(handle))); diff --git a/src/yaml_c_wrapper.h b/src/yaml_c_wrapper.h index e4c5e05..319a9cb 100644 --- a/src/yaml_c_wrapper.h +++ b/src/yaml_c_wrapper.h @@ -13,6 +13,7 @@ typedef void* YAMLNodeHandle; YAMLNodeHandle yaml_create_node(void); YAMLNodeHandle yaml_create_map(void); YAMLNodeHandle yaml_create_sequence(void); +YAMLNodeHandle yaml_create_scalar(); void yaml_delete_node(YAMLNodeHandle handle); // === PARSING === @@ -24,7 +25,6 @@ bool yaml_is_scalar(YAMLNodeHandle handle); bool yaml_is_sequence(YAMLNodeHandle handle); bool yaml_is_map(YAMLNodeHandle handle); bool yaml_is_null(YAMLNodeHandle handle); -bool yaml_is_defined(YAMLNodeHandle handle); // === ACCESS === YAMLNodeHandle yaml_get_key(YAMLNodeHandle handle, const char* key); @@ -46,6 +46,12 @@ void yaml_set_float(YAMLNodeHandle handle, const char* key, double value); void yaml_set_bool(YAMLNodeHandle handle, const char* key, bool value); void yaml_set_node(YAMLNodeHandle handle, const char* key, YAMLNodeHandle value); +void yaml_set_scalar_string(YAMLNodeHandle handle, const char* value); +void yaml_set_scalar_int(YAMLNodeHandle handle, int value); +void yaml_set_scalar_float(YAMLNodeHandle handle, double value); +void yaml_set_scalar_bool(YAMLNodeHandle handle, bool value); + +void yaml_set_at_index(YAMLNodeHandle handle, int index, YAMLNodeHandle value) ; void yaml_push_string(YAMLNodeHandle handle, const char* value); void yaml_push_int(YAMLNodeHandle handle, int value); void yaml_push_float(YAMLNodeHandle handle, double value); diff --git a/src/yaml_reader.cpp b/src/yaml_reader.cpp deleted file mode 100644 index 35226ff..0000000 --- a/src/yaml_reader.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "yaml_c_wrapper.h" -#include - -int main() { - YAMLNodeHandle handle = yaml_parse_file("../lattice_files/ex.yaml"); - handle = yaml_expand(handle); - yaml_write_file(handle, "../lattice_files/expand.yaml"); - - // type checking: - std::cout << (yaml_is_sequence(handle)) << "\n"; - - // access: - std::cout << yaml_to_string(yaml_get_index(handle, 0)) << "\n"; -} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..3cf143d --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +include(CTest) +add_executable(tests + test_yaml_c_wrapper.cpp +) + +target_link_libraries(tests + PRIVATE + yaml_c_wrapper + Catch2::Catch2WithMain +) + +include(Catch) +Catch_discover_tests(tests) +target_compile_features(tests PRIVATE cxx_std_17) diff --git a/tests/test_yaml_c_wrapper.cpp b/tests/test_yaml_c_wrapper.cpp new file mode 100644 index 0000000..1fcb5f6 --- /dev/null +++ b/tests/test_yaml_c_wrapper.cpp @@ -0,0 +1,745 @@ +#define CATCH_CONFIG_MAIN +#include +#include +#include "../src/yaml_c_wrapper.h" +#include +#include +#include + +using Catch::Approx; + +// Helper to create test YAML files +void create_test_file(const std::string& filename, const std::string& content) { + std::ofstream file(filename); + file << content; + file.close(); +} + +// Helper to read file content +std::string read_file(const std::string& filename) { + std::ifstream file(filename); + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + return content; +} + +// Helper to clean up test files +void cleanup_file(const std::string& filename) { + std::remove(filename.c_str()); +} + +// =========================================== +// TEST SUITE: Creation and Deletion +// =========================================== + +TEST_CASE("YAML nodes can be created and deleted", "[creation]") { + SECTION("Create empty node") { + YAMLNodeHandle node = yaml_create_node(); + REQUIRE(node != nullptr); + yaml_delete_node(node); + } + + SECTION("Create map node") { + YAMLNodeHandle map = yaml_create_map(); + REQUIRE(map != nullptr); + REQUIRE(yaml_is_map(map)); + yaml_delete_node(map); + } + + SECTION("Create sequence node") { + YAMLNodeHandle seq = yaml_create_sequence(); + REQUIRE(seq != nullptr); + REQUIRE(yaml_is_sequence(seq)); + yaml_delete_node(seq); + } + + SECTION("Create scalar node") { + YAMLNodeHandle scalar = yaml_create_scalar(); + REQUIRE(scalar != nullptr); + REQUIRE(yaml_is_scalar(scalar)); + yaml_delete_node(scalar); + } +} + +// =========================================== +// TEST SUITE: Parsing +// =========================================== + +TEST_CASE("YAML can be parsed from strings", "[parsing]") { + SECTION("Parse simple map") { + const char* yaml = "key: value"; + YAMLNodeHandle node = yaml_parse(yaml); + + REQUIRE(node != nullptr); + REQUIRE(yaml_is_map(node)); + REQUIRE(yaml_has_key(node, "key")); + + yaml_delete_node(node); + } + + SECTION("Parse sequence") { + const char* yaml = "[a, b, c]"; + YAMLNodeHandle node = yaml_parse(yaml); + + REQUIRE(node != nullptr); + REQUIRE(yaml_is_sequence(node)); + REQUIRE(yaml_size(node) == 3); + + yaml_delete_node(node); + } + + SECTION("Parse invalid YAML returns nullptr") { + const char* invalid = "invalid: yaml: :"; + YAMLNodeHandle node = yaml_parse(invalid); + + REQUIRE(node == nullptr); + } +} + +TEST_CASE("YAML can be parsed from files", "[parsing][file]") { + SECTION("Parse valid file") { + YAMLNodeHandle node = yaml_parse_file("../lattice_files/ex.yaml"); + + REQUIRE(node != nullptr); + REQUIRE(yaml_is_sequence(node)); + REQUIRE(yaml_size(node) >= 2); + + yaml_delete_node(node); + } + + SECTION("Parse non-existent file returns nullptr") { + YAMLNodeHandle node = yaml_parse_file("nonexistent.yaml"); + REQUIRE(node == nullptr); + } +} + +// =========================================== +// TEST SUITE: Type Checking +// =========================================== + +TEST_CASE("Node types can be checked", "[types]") { + SECTION("Check scalar type") { + YAMLNodeHandle node = yaml_parse("value"); + REQUIRE(yaml_is_scalar(node)); + REQUIRE_FALSE(yaml_is_map(node)); + REQUIRE_FALSE(yaml_is_sequence(node)); + yaml_delete_node(node); + } + + SECTION("Check map type") { + YAMLNodeHandle node = yaml_parse("key: value"); + REQUIRE(yaml_is_map(node)); + REQUIRE_FALSE(yaml_is_scalar(node)); + REQUIRE_FALSE(yaml_is_sequence(node)); + yaml_delete_node(node); + } + + SECTION("Check sequence type") { + YAMLNodeHandle node = yaml_parse("[a, b, c]"); + REQUIRE(yaml_is_sequence(node)); + REQUIRE_FALSE(yaml_is_map(node)); + REQUIRE_FALSE(yaml_is_scalar(node)); + yaml_delete_node(node); + } + + SECTION("Check null type") { + YAMLNodeHandle node = yaml_parse("null"); + REQUIRE(yaml_is_null(node)); + yaml_delete_node(node); + } +} + +// =========================================== +// TEST SUITE: Access Operations +// =========================================== + +TEST_CASE("Map keys can be accessed", "[access][map]") { + const char* yaml = "name: test\nvalue: 42"; + YAMLNodeHandle node = yaml_parse(yaml); + + SECTION("Check key existence") { + REQUIRE(yaml_has_key(node, "name")); + REQUIRE(yaml_has_key(node, "value")); + REQUIRE_FALSE(yaml_has_key(node, "nonexistent")); + } + + SECTION("Get key value") { + YAMLNodeHandle name = yaml_get_key(node, "name"); + REQUIRE(name != nullptr); + + char* str = yaml_as_string(name); + REQUIRE(std::string(str) == "test"); + + yaml_free_string(str); + yaml_delete_node(name); + } + + SECTION("Get non-existent key returns nullptr") { + YAMLNodeHandle missing = yaml_get_key(node, "missing"); + REQUIRE(missing == nullptr); + } + + SECTION("Get all keys") { + int count; + char** keys = yaml_get_keys(node, &count); + + REQUIRE(count == 2); + REQUIRE(keys != nullptr); + + bool has_name = false, has_value = false; + for (int i = 0; i < count; i++) { + if (std::string(keys[i]) == "name") has_name = true; + if (std::string(keys[i]) == "value") has_value = true; + } + + REQUIRE(has_name); + REQUIRE(has_value); + + yaml_free_keys(keys, count); + } + + yaml_delete_node(node); +} + +TEST_CASE("Sequence indices can be accessed", "[access][sequence]") { + const char* yaml = "[apple, banana, cherry]"; + YAMLNodeHandle node = yaml_parse(yaml); + + SECTION("Check size") { + REQUIRE(yaml_size(node) == 3); + } + + SECTION("Access valid index") { + YAMLNodeHandle item = yaml_get_index(node, 1); + REQUIRE(item != nullptr); + + char* str = yaml_as_string(item); + REQUIRE(std::string(str) == "banana"); + + yaml_free_string(str); + yaml_delete_node(item); + } + + SECTION("Access out of bounds index returns nullptr") { + YAMLNodeHandle item = yaml_get_index(node, 999); + REQUIRE(item == nullptr); + } + + SECTION("Access negative index returns nullptr") { + YAMLNodeHandle item = yaml_get_index(node, -1); + REQUIRE(item == nullptr); + } + + yaml_delete_node(node); +} + +// =========================================== +// TEST SUITE: Type Conversions +// =========================================== + +TEST_CASE("Values can be converted to C types", "[conversion]") { + SECTION("Convert to string") { + YAMLNodeHandle node = yaml_parse("test_value"); + char* str = yaml_as_string(node); + + REQUIRE(str != nullptr); + REQUIRE(std::string(str) == "test_value"); + + yaml_free_string(str); + yaml_delete_node(node); + } + + SECTION("Convert to int") { + YAMLNodeHandle node = yaml_parse("42"); + int val = yaml_as_int(node); + + REQUIRE(val == 42); + + yaml_delete_node(node); + } + + SECTION("Convert to float") { + YAMLNodeHandle node = yaml_parse("3.14"); + double val = yaml_as_float(node); + + REQUIRE(val == Approx(3.14)); + + yaml_delete_node(node); + } + + SECTION("Convert to bool") { + YAMLNodeHandle node_true = yaml_parse("true"); + YAMLNodeHandle node_false = yaml_parse("false"); + + REQUIRE(yaml_as_bool(node_true) == true); + REQUIRE(yaml_as_bool(node_false) == false); + + yaml_delete_node(node_true); + yaml_delete_node(node_false); + } + + SECTION("Invalid conversion returns default") { + YAMLNodeHandle node = yaml_parse("[a, b, c]"); + + // Can't convert sequence to string + char* str = yaml_as_string(node); + REQUIRE(str == nullptr); + + yaml_delete_node(node); + } +} + +// =========================================== +// TEST SUITE: Modification - Maps +// =========================================== + +TEST_CASE("Map values can be set", "[modification][map]") { + YAMLNodeHandle map = yaml_create_map(); + + SECTION("Set string value") { + yaml_set_string(map, "name", "test"); + REQUIRE(yaml_has_key(map, "name")); + + YAMLNodeHandle value = yaml_get_key(map, "name"); + char* str = yaml_as_string(value); + REQUIRE(std::string(str) == "test"); + + yaml_free_string(str); + yaml_delete_node(value); + } + + SECTION("Set int value") { + yaml_set_int(map, "count", 42); + + YAMLNodeHandle value = yaml_get_key(map, "count"); + REQUIRE(yaml_as_int(value) == 42); + + yaml_delete_node(value); + } + + SECTION("Set float value") { + yaml_set_float(map, "pi", 3.14); + + YAMLNodeHandle value = yaml_get_key(map, "pi"); + REQUIRE(yaml_as_float(value) == Catch::Approx(3.14)); + + yaml_delete_node(value); + } + + SECTION("Set bool value") { + yaml_set_bool(map, "enabled", true); + + YAMLNodeHandle value = yaml_get_key(map, "enabled"); + REQUIRE(yaml_as_bool(value) == true); + + yaml_delete_node(value); + } + + SECTION("Set nested node") { + YAMLNodeHandle nested = yaml_create_map(); + yaml_set_string(nested, "inner", "value"); + + yaml_set_node(map, "nested", nested); + + REQUIRE(yaml_has_key(map, "nested")); + YAMLNodeHandle retrieved = yaml_get_key(map, "nested"); + REQUIRE(yaml_is_map(retrieved)); + + yaml_delete_node(retrieved); + yaml_delete_node(nested); + } + + yaml_delete_node(map); +} + +// =========================================== +// TEST SUITE: Modification - Sequences +// =========================================== + +TEST_CASE("Sequence values can be pushed", "[modification][sequence]") { + YAMLNodeHandle seq = yaml_create_sequence(); + + SECTION("Push string values") { + yaml_push_string(seq, "first"); + yaml_push_string(seq, "second"); + + REQUIRE(yaml_size(seq) == 2); + + YAMLNodeHandle item = yaml_get_index(seq, 1); + char* str = yaml_as_string(item); + REQUIRE(std::string(str) == "second"); + + yaml_free_string(str); + yaml_delete_node(item); + } + + SECTION("Push int values") { + yaml_push_int(seq, 10); + yaml_push_int(seq, 20); + + REQUIRE(yaml_size(seq) == 2); + + YAMLNodeHandle item = yaml_get_index(seq, 0); + REQUIRE(yaml_as_int(item) == 10); + + yaml_delete_node(item); + } + + SECTION("Push float values") { + yaml_push_float(seq, 1.1); + yaml_push_float(seq, 2.2); + + REQUIRE(yaml_size(seq) == 2); + + YAMLNodeHandle item = yaml_get_index(seq, 1); + REQUIRE(yaml_as_float(item) == Catch::Approx(2.2)); + + yaml_delete_node(item); + } + + SECTION("Push node") { + YAMLNodeHandle node = yaml_create_map(); + yaml_set_string(node, "key", "value"); + + yaml_push_node(seq, node); + + REQUIRE(yaml_size(seq) == 1); + + YAMLNodeHandle retrieved = yaml_get_index(seq, 0); + REQUIRE(yaml_is_map(retrieved)); + + yaml_delete_node(retrieved); + yaml_delete_node(node); + } + + yaml_delete_node(seq); +} + +TEST_CASE("Sequence values can be set at index", "[modification][sequence]") { + YAMLNodeHandle seq = yaml_parse("[a, b, c]"); + + SECTION("Set string at index") { + YAMLNodeHandle replacement = yaml_parse("replaced"); + yaml_set_at_index(seq, 1, replacement); + + YAMLNodeHandle item = yaml_get_index(seq, 1); + char* str = yaml_as_string(item); + REQUIRE(std::string(str) == "replaced"); + + yaml_free_string(str); + yaml_delete_node(item); + yaml_delete_node(replacement); + } + + yaml_delete_node(seq); +} + +// =========================================== +// TEST SUITE: Scalar Editing +// =========================================== + +TEST_CASE("Scalar values can be edited directly", "[modification][scalar]") { + SECTION("Set scalar string") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_string(scalar, "new_value"); + + char* str = yaml_as_string(scalar); + REQUIRE(std::string(str) == "new_value"); + + yaml_free_string(str); + yaml_delete_node(scalar); + } + + SECTION("Set scalar int") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_int(scalar, 99); + + REQUIRE(yaml_as_int(scalar) == 99); + + yaml_delete_node(scalar); + } + + SECTION("Set scalar float") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_float(scalar, 2.718); + + REQUIRE(yaml_as_float(scalar) == Catch::Approx(2.718)); + + yaml_delete_node(scalar); + } + + SECTION("Set scalar bool") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_bool(scalar, false); + + REQUIRE(yaml_as_bool(scalar) == false); + + yaml_delete_node(scalar); + } +} + +// =========================================== +// TEST SUITE: File I/O +// =========================================== + +TEST_CASE("YAML can be written to files", "[io][file]") { + const char* test_file = "test_output.yaml"; + + SECTION("Write simple map") { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_string(map, "test", "value"); + yaml_set_int(map, "count", 5); + + REQUIRE(yaml_write_file(map, test_file)); + + // Read back and verify + YAMLNodeHandle loaded = yaml_parse_file(test_file); + REQUIRE(loaded != nullptr); + REQUIRE(yaml_has_key(loaded, "test")); + REQUIRE(yaml_has_key(loaded, "count")); + + yaml_delete_node(map); + yaml_delete_node(loaded); + cleanup_file(test_file); + } + + SECTION("Write with formatting") { + YAMLNodeHandle seq = yaml_parse("[a, b, c]"); + + REQUIRE(yaml_write_file_formatted(seq, test_file, 4, false, true)); + + std::string content = read_file(test_file); + REQUIRE(content.find("[a, b, c]") != std::string::npos); // Flow style + + yaml_delete_node(seq); + cleanup_file(test_file); + } +} + +// =========================================== +// TEST SUITE: String Conversion +// =========================================== + +TEST_CASE("YAML nodes can be converted to strings", "[conversion][string]") { + SECTION("Convert map to string") { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_string(map, "key", "value"); + + char* str = yaml_to_string(map); + REQUIRE(str != nullptr); + REQUIRE(std::string(str).find("key") != std::string::npos); + REQUIRE(std::string(str).find("value") != std::string::npos); + + yaml_free_string(str); + yaml_delete_node(map); + } + + SECTION("Emit with custom indent") { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_string(map, "test", "val"); + + char* str2 = yaml_emit(map, 2); + char* str4 = yaml_emit(map, 4); + + REQUIRE(str2 != nullptr); + REQUIRE(str4 != nullptr); + + yaml_free_string(str2); + yaml_free_string(str4); + yaml_delete_node(map); + } +} + +// =========================================== +// TEST SUITE: Cloning +// =========================================== + +TEST_CASE("YAML nodes can be cloned", "[clone]") { + SECTION("Clone simple map") { + YAMLNodeHandle original = yaml_create_map(); + yaml_set_string(original, "name", "original"); + + YAMLNodeHandle clone = yaml_clone(original); + REQUIRE(clone != nullptr); + + // Modify clone + yaml_set_string(clone, "name", "modified"); + + // Verify original unchanged + YAMLNodeHandle orig_val = yaml_get_key(original, "name"); + char* orig_str = yaml_as_string(orig_val); + REQUIRE(std::string(orig_str) == "original"); + + // Verify clone changed + YAMLNodeHandle clone_val = yaml_get_key(clone, "name"); + char* clone_str = yaml_as_string(clone_val); + REQUIRE(std::string(clone_str) == "modified"); + + yaml_free_string(orig_str); + yaml_free_string(clone_str); + yaml_delete_node(orig_val); + yaml_delete_node(clone_val); + yaml_delete_node(original); + yaml_delete_node(clone); + } + + SECTION("Clone nested structure") { + YAMLNodeHandle original = yaml_parse("outer: {inner: value}"); + YAMLNodeHandle clone = yaml_clone(original); + + REQUIRE(clone != nullptr); + REQUIRE(yaml_is_map(clone)); + + YAMLNodeHandle outer = yaml_get_key(clone, "outer"); + REQUIRE(yaml_has_key(outer, "inner")); + + yaml_delete_node(outer); + yaml_delete_node(original); + yaml_delete_node(clone); + } +} + +// =========================================== +// TEST SUITE: ex.yaml Structure Tests +// =========================================== + +TEST_CASE("ex.yaml has expected structure", "[ex.yaml][structure]") { + YAMLNodeHandle root = yaml_parse_file("../lattice_files/ex.yaml"); + REQUIRE(root != nullptr); + + SECTION("Root is a sequence") { + REQUIRE(yaml_is_sequence(root)); + REQUIRE(yaml_size(root) >= 2); + } + + SECTION("First element has thingB") { + YAMLNodeHandle first = yaml_get_index(root, 0); + REQUIRE(first != nullptr); + REQUIRE(yaml_is_map(first)); + REQUIRE(yaml_has_key(first, "thingB")); + + YAMLNodeHandle thingB = yaml_get_key(first, "thingB"); + REQUIRE(yaml_is_map(thingB)); + REQUIRE(yaml_has_key(thingB, "kind")); + + YAMLNodeHandle kind = yaml_get_key(thingB, "kind"); + char* kind_str = yaml_as_string(kind); + REQUIRE(std::string(kind_str) == "Sextupole"); + + yaml_free_string(kind_str); + yaml_delete_node(kind); + yaml_delete_node(thingB); + yaml_delete_node(first); + } + + SECTION("Second element has inj_line") { + YAMLNodeHandle second = yaml_get_index(root, 1); + REQUIRE(second != nullptr); + REQUIRE(yaml_is_map(second)); + REQUIRE(yaml_has_key(second, "inj_line")); + + YAMLNodeHandle inj_line = yaml_get_key(second, "inj_line"); + REQUIRE(yaml_is_map(inj_line)); + REQUIRE(yaml_has_key(inj_line, "kind")); + REQUIRE(yaml_has_key(inj_line, "multipass")); + REQUIRE(yaml_has_key(inj_line, "line")); + + YAMLNodeHandle kind = yaml_get_key(inj_line, "kind"); + char* kind_str = yaml_as_string(kind); + REQUIRE(std::string(kind_str) == "BeamLine"); + + YAMLNodeHandle multipass = yaml_get_key(inj_line, "multipass"); + REQUIRE(yaml_as_bool(multipass) == true); + + yaml_free_string(kind_str); + yaml_delete_node(kind); + yaml_delete_node(multipass); + yaml_delete_node(inj_line); + yaml_delete_node(second); + } + + SECTION("inj_line has line sequence") { + YAMLNodeHandle second = yaml_get_index(root, 1); + YAMLNodeHandle inj_line = yaml_get_key(second, "inj_line"); + YAMLNodeHandle line = yaml_get_key(inj_line, "line"); + + REQUIRE(yaml_is_sequence(line)); + REQUIRE(yaml_size(line) >= 3); + + yaml_delete_node(line); + yaml_delete_node(inj_line); + yaml_delete_node(second); + } + + yaml_delete_node(root); +} + +// =========================================== +// TEST SUITE: Memory Safety +// =========================================== + +TEST_CASE("Memory is properly managed", "[memory]") { + SECTION("Can safely delete nullptr") { + yaml_delete_node(nullptr); // Should not crash + } + + SECTION("Multiple operations don't leak") { + for (int i = 0; i < 100; i++) { + YAMLNodeHandle node = yaml_create_map(); + yaml_set_int(node, "test", i); + + char* str = yaml_to_string(node); + yaml_free_string(str); + + yaml_delete_node(node); + } + // No assertion - just shouldn't crash or leak + } + + SECTION("Complex structure cleanup") { + YAMLNodeHandle root = yaml_create_sequence(); + + for (int i = 0; i < 10; i++) { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_int(map, "id", i); + yaml_push_node(root, map); + yaml_delete_node(map); // Safe after push_node copies + } + + yaml_delete_node(root); + } +} + +// =========================================== +// TEST SUITE: Edge Cases +// =========================================== + +TEST_CASE("Edge cases are handled correctly", "[edge_cases]") { + SECTION("Empty map") { + YAMLNodeHandle map = yaml_create_map(); + REQUIRE(yaml_size(map) == 0); + + int count; + char** keys = yaml_get_keys(map, &count); + REQUIRE(count == 0); + + yaml_delete_node(map); + } + + SECTION("Empty sequence") { + YAMLNodeHandle seq = yaml_create_sequence(); + REQUIRE(yaml_size(seq) == 0); + + YAMLNodeHandle item = yaml_get_index(seq, 0); + REQUIRE(item == nullptr); + + yaml_delete_node(seq); + } + + SECTION("Set scalar to nullptr is safe") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_string(nullptr, "test"); // Should not crash + yaml_set_scalar_string(scalar, nullptr); // Should not crash + yaml_delete_node(scalar); + } +} \ No newline at end of file