Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions include/c2pa.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 16 additions & 3 deletions src/c2pa.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -477,20 +477,33 @@ inline std::vector<unsigned char> 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<std::ifstream>(settings_path);
std::string json_content((std::istreambuf_iterator<char>(*file)), std::istreambuf_iterator<char>());

// 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);
}

Expand Down
110 changes: 58 additions & 52 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}")

57 changes: 46 additions & 11 deletions tests/context.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -339,23 +335,23 @@ 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"));

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"));

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");
Expand All @@ -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}}})")
Expand All @@ -378,15 +374,15 @@ 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"));

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");
Expand All @@ -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));
}