diff --git a/Makefile b/Makefile index 02aafc13..73c75771 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,7 @@ else endif # Test with coverage reporting +# THis verifies necessary tooling for coverage check is also installed test-coverage: clean cmake -S . -B build/coverage -G "Ninja" -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON $(CMAKE_OPTS) cmake --build build/coverage diff --git a/include/c2pa.hpp b/include/c2pa.hpp index 16cdfc82..176c6a99 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -279,6 +279,12 @@ namespace c2pa /// @throws C2paException if JSON is invalid. ContextBuilder& with_json(const std::string& json); + /// @brief Configure settings from a JSON settings file. + /// @param settings_path Full path to the JSON settings file. + /// @return Reference to this ContextBuilder for method chaining. + /// @throws C2paException if file cannot be read or JSON is invalid. + ContextBuilder& with_json_settings_file(const std::filesystem::path& settings_path); + /// @brief Create a Context from the current builder configuration. /// @return A new Context instance. /// @throws C2paException if context creation fails. diff --git a/src/c2pa.cpp b/src/c2pa.cpp index b853d3fb..38169dfa 100644 --- a/src/c2pa.cpp +++ b/src/c2pa.cpp @@ -477,20 +477,33 @@ inline std::vector to_byte_vector(const unsigned char* data, int6 return with_settings(Settings(json, "json")); } + Context::ContextBuilder& Context::ContextBuilder::with_json_settings_file(const std::filesystem::path& settings_path) { + if (!is_valid()) { + throw C2paException("ContextBuilder is invalid (moved from)"); + } + + // Open the file and read its content + auto file = detail::open_file_binary(settings_path); + std::string json_content((std::istreambuf_iterator(*file)), std::istreambuf_iterator()); + + // Use the existing with_json method + return with_json(json_content); + } + Context Context::ContextBuilder::create_context() { if (!is_valid()) { throw C2paException("ContextBuilder is invalid (moved from)"); } - + // The C API consumes the builder on build C2paContext* ctx = c2pa_context_builder_build(context_builder); if (!ctx) { throw C2paException("Failed to build context"); } - + // Builder is consumed by the C API context_builder = nullptr; - + return Context(ctx); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f45893a0..7ea0ee9c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,39 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(googletest) +# ============================================================================ +# Platform-specific configuration variables +# ============================================================================ + +# Environment variables for test runtime +set(LSAN_SUPPRESSIONS_FILE "${c2pa-c_SOURCE_DIR}/ci-cd/lsan_suppressions.txt") + +if(APPLE) + set(TEST_LIBRARY_PATH_VAR "DYLD_LIBRARY_PATH") + set(TEST_LIBRARY_PATH_VALUE "${c2pa-c_BINARY_DIR}/tests:$ENV{DYLD_LIBRARY_PATH}") +elseif(UNIX) + set(TEST_LIBRARY_PATH_VAR "LD_LIBRARY_PATH") + set(TEST_LIBRARY_PATH_VALUE "${c2pa-c_BINARY_DIR}/tests:$ENV{LD_LIBRARY_PATH};LSAN_OPTIONS=suppressions=${LSAN_SUPPRESSIONS_FILE}") +endif() + +# ============================================================================ +# Utility functions +# ============================================================================ + +# Function to configure platform-specific test environment +function(setup_test_environment test_name working_dir) + set_tests_properties(${test_name} PROPERTIES + WORKING_DIRECTORY "${working_dir}" + ) + + # Set platform-specific library path environment + if(DEFINED TEST_LIBRARY_PATH_VAR) + set_tests_properties(${test_name} PROPERTIES + ENVIRONMENT "${TEST_LIBRARY_PATH_VAR}=${TEST_LIBRARY_PATH_VALUE}" + ) + endif() +endfunction() + # Function to setup runtime dependencies for any target function(setup_c2pa_runtime_deps target_name) # Copy shared library to executable directory on all platforms @@ -37,32 +70,32 @@ function(setup_c2pa_runtime_deps target_name) endif() endif() - # On Linux, set RPATH to look in the executable's directory - if(UNIX AND NOT APPLE) - set_target_properties(${target_name} PROPERTIES - BUILD_RPATH "$ORIGIN" - INSTALL_RPATH "$ORIGIN" - BUILD_WITH_INSTALL_RPATH TRUE - SKIP_BUILD_RPATH FALSE - # Clear any automatic RPATH from linked libraries - LINK_WHAT_YOU_USE FALSE - ) - # Force override any inherited RPATH - set_property(TARGET ${target_name} PROPERTY BUILD_RPATH "$ORIGIN") - set_property(TARGET ${target_name} PROPERTY INSTALL_RPATH "$ORIGIN") - endif() - - # On macOS, override the RPATH to remove hardcoded paths - if(APPLE) + # On Unix systems (Linux/macOS), set RPATH to look in the executable's directory + if(UNIX) + # Platform-specific RPATH syntax + if(APPLE) + set(RPATH_VALUE "@executable_path") + else() + set(RPATH_VALUE "$ORIGIN") + endif() + set_target_properties(${target_name} PROPERTIES - BUILD_RPATH "@executable_path" - INSTALL_RPATH "@executable_path" + BUILD_RPATH "${RPATH_VALUE}" + INSTALL_RPATH "${RPATH_VALUE}" BUILD_WITH_INSTALL_RPATH TRUE SKIP_BUILD_RPATH FALSE ) + + # On Linux, clear any automatic RPATH from linked libraries + if(NOT APPLE) + set_target_properties(${target_name} PROPERTIES + LINK_WHAT_YOU_USE FALSE + ) + endif() + # Force override any inherited RPATH - set_property(TARGET ${target_name} PROPERTY BUILD_RPATH "@executable_path") - set_property(TARGET ${target_name} PROPERTY INSTALL_RPATH "@executable_path") + set_property(TARGET ${target_name} PROPERTY BUILD_RPATH "${RPATH_VALUE}") + set_property(TARGET ${target_name} PROPERTY INSTALL_RPATH "${RPATH_VALUE}") endif() endfunction() @@ -104,23 +137,8 @@ else() add_test(NAME cpp_tests COMMAND c2pa_c_tests) endif() -set_tests_properties(cpp_tests PROPERTIES - WORKING_DIRECTORY "${c2pa-c_BINARY_DIR}/tests" -) - -# Set platform-specific environment -# On Linux with sanitizers, also set LSAN_OPTIONS to suppress known Tokio runtime -# initialization leaks inside the prebuilt libc2pa_c Rust library. -set(LSAN_SUPPRESSIONS_FILE "${c2pa-c_SOURCE_DIR}/ci-cd/lsan_suppressions.txt") -if(APPLE) - set_tests_properties(cpp_tests PROPERTIES - ENVIRONMENT "DYLD_LIBRARY_PATH=${c2pa-c_BINARY_DIR}/tests:$ENV{DYLD_LIBRARY_PATH}" - ) -elseif(UNIX) - set_tests_properties(cpp_tests PROPERTIES - ENVIRONMENT "LD_LIBRARY_PATH=${c2pa-c_BINARY_DIR}/tests:$ENV{LD_LIBRARY_PATH};LSAN_OPTIONS=suppressions=${LSAN_SUPPRESSIONS_FILE}" - ) -endif() +# Set platform-specific environment and working directory +setup_test_environment(cpp_tests "${c2pa-c_BINARY_DIR}/tests") # C tests add_executable(ctest c-app-test/test.c) @@ -161,18 +179,6 @@ setup_c2pa_runtime_deps(ctest) # Register tests with CTest add_test(NAME c_test COMMAND ctest) -set_tests_properties(c_test PROPERTIES - WORKING_DIRECTORY "${c2pa-c_SOURCE_DIR}" -) - -# Set platform-specific environment for C test -if(APPLE) - set_tests_properties(c_test PROPERTIES - ENVIRONMENT "DYLD_LIBRARY_PATH=${c2pa-c_BINARY_DIR}/tests:$ENV{DYLD_LIBRARY_PATH}" - ) -elseif(UNIX) - set_tests_properties(c_test PROPERTIES - ENVIRONMENT "LD_LIBRARY_PATH=${c2pa-c_BINARY_DIR}/tests:$ENV{LD_LIBRARY_PATH};LSAN_OPTIONS=suppressions=${LSAN_SUPPRESSIONS_FILE}" - ) -endif() +# Set platform-specific environment and working directory +setup_test_environment(c_test "${c2pa-c_SOURCE_DIR}") diff --git a/tests/context.test.cpp b/tests/context.test.cpp index b4cab0ce..b4d816fa 100644 --- a/tests/context.test.cpp +++ b/tests/context.test.cpp @@ -324,11 +324,7 @@ TEST(Context, DirectConstructDefaultWithBuilder) { }); } -// ============================================================================= -// All creation variations: sign + verify settings propagate -// ============================================================================= - -// 1) Direct construction with Settings — sign and verify thumbnail is disabled +// 1) Direct construction with Settings: sign and verify thumbnail is disabled TEST_F(ContextTest, DirectConstructSettingsSignVerify) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "false"); @@ -339,7 +335,7 @@ TEST_F(ContextTest, DirectConstructSettingsSignVerify) { EXPECT_FALSE(has_thumbnail(manifest_json)); } -// 2) Direct default construction — sign and verify thumbnail is enabled (default) +// 2) Direct default construction: sign and verify thumbnail is enabled (default) TEST_F(ContextTest, DirectConstructDefaultSignVerify) { c2pa::Context context; auto manifest_json = sign_with_context(context, get_temp_path("direct_construct_default.jpg")); @@ -347,7 +343,7 @@ TEST_F(ContextTest, DirectConstructDefaultSignVerify) { EXPECT_TRUE(has_thumbnail(manifest_json)); } -// 3) JSON string constructor — sign and verify thumbnail is disabled +// 3) JSON string constructor: sign and verify thumbnail is disabled TEST_F(ContextTest, JsonConstructorSignVerify) { c2pa::Context context(R"({"builder": {"thumbnail": {"enabled": false}}})"); auto manifest_json = sign_with_context(context, get_temp_path("json_constructor.jpg")); @@ -355,7 +351,7 @@ TEST_F(ContextTest, JsonConstructorSignVerify) { EXPECT_FALSE(has_thumbnail(manifest_json)); } -// 4) ContextBuilder with Settings — sign and verify thumbnail is disabled +// 4) ContextBuilder with Settings: sign and verify thumbnail is disabled TEST_F(ContextTest, ContextBuilderWithSettingsSignVerify) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "false"); @@ -368,7 +364,7 @@ TEST_F(ContextTest, ContextBuilderWithSettingsSignVerify) { EXPECT_FALSE(has_thumbnail(manifest_json)); } -// 5) ContextBuilder with JSON — sign and verify thumbnail is disabled +// 5) ContextBuilder with JSON: sign and verify thumbnail is disabled TEST_F(ContextTest, ContextBuilderWithJsonSignVerify) { auto context = c2pa::Context::ContextBuilder() .with_json(R"({"builder": {"thumbnail": {"enabled": false}}})") @@ -378,7 +374,7 @@ TEST_F(ContextTest, ContextBuilderWithJsonSignVerify) { EXPECT_FALSE(has_thumbnail(manifest_json)); } -// 6) ContextBuilder empty (default) — sign and verify thumbnail is enabled (default) +// 6) ContextBuilder empty (default): sign and verify thumbnail is enabled (default) TEST_F(ContextTest, ContextBuilderDefaultSignVerify) { auto context = c2pa::Context::ContextBuilder().create_context(); auto manifest_json = sign_with_context(context, get_temp_path("builder_default.jpg")); @@ -386,7 +382,7 @@ TEST_F(ContextTest, ContextBuilderDefaultSignVerify) { EXPECT_TRUE(has_thumbnail(manifest_json)); } -// 7) Direct construction with Settings enabling thumbnail — sign and verify +// 7) Direct construction with Settings enabling thumbnail: sign and verify TEST_F(ContextTest, DirectConstructSettingsEnableThumbnailSignVerify) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "true"); @@ -396,3 +392,42 @@ TEST_F(ContextTest, DirectConstructSettingsEnableThumbnailSignVerify) { EXPECT_TRUE(has_thumbnail(manifest_json)); } + +// Test with_json_settings_file method: loads settings from file path +TEST_F(ContextTest, ContextBuilderWithJsonSettingsFile) { + fs::path current_dir = fs::path(__FILE__).parent_path(); + fs::path settings_path = current_dir / "fixtures/settings/test_settings_no_thumbnail.json"; + + auto context = c2pa::Context::ContextBuilder() + .with_json_settings_file(settings_path) + .create_context(); + + auto manifest_json = sign_with_context(context, get_temp_path("with_json_settings_file.jpg")); + EXPECT_FALSE(has_thumbnail(manifest_json)); +} + +// Test with_json_settings_file with invalid path throws exception +TEST(Context, ContextBuilderWithJsonSettingsFileInvalidPath) { + auto builder = c2pa::Context::ContextBuilder(); + EXPECT_THROW({ + builder.with_json_settings_file("/nonexistent/path/to/settings.json"); + }, c2pa::C2paException); +} + +// Test with_json_settings_file can be chained with other methods +TEST_F(ContextTest, ContextBuilderWithJsonSettingsFileChaining) { + fs::path current_dir = fs::path(__FILE__).parent_path(); + fs::path settings_path = current_dir / "fixtures/settings/test_settings_with_thumbnail.json"; + + // File enables thumbnail, then we disable it with set() + c2pa::Settings override_settings; + override_settings.set("builder.thumbnail.enabled", "false"); + + auto context = c2pa::Context::ContextBuilder() + .with_json_settings_file(settings_path) + .with_settings(override_settings) + .create_context(); + + auto manifest_json = sign_with_context(context, get_temp_path("with_json_settings_file_chained.jpg")); + EXPECT_FALSE(has_thumbnail(manifest_json)); +}