diff --git a/tests/unit_tests/check/elements/CMakeLists.txt b/tests/unit_tests/check/elements/CMakeLists.txt index b361745a0..d062e4b82 100644 --- a/tests/unit_tests/check/elements/CMakeLists.txt +++ b/tests/unit_tests/check/elements/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================== -# Copyright (C) 2018-2025 Intel Corporation +# Copyright (C) 2018-2026 Intel Corporation # # SPDX-License-Identifier: MIT # ============================================================================== @@ -10,6 +10,7 @@ add_subdirectory(classification) add_subdirectory(metaconvert) add_subdirectory(metapublish) add_subdirectory(fpscounter) +add_subdirectory(watermark) add_subdirectory(properties) if(${ENABLE_AUDIO_INFERENCE_ELEMENTS}) diff --git a/tests/unit_tests/check/elements/watermark/CMakeLists.txt b/tests/unit_tests/check/elements/watermark/CMakeLists.txt new file mode 100644 index 000000000..9859b9b9c --- /dev/null +++ b/tests/unit_tests/check/elements/watermark/CMakeLists.txt @@ -0,0 +1,8 @@ +# ============================================================================== +# Copyright (C) 2026 Intel Corporation +# +# SPDX-License-Identifier: MIT +# ============================================================================== + +add_subdirectory(test_watermark) +add_subdirectory(test_properties) diff --git a/tests/unit_tests/check/elements/watermark/test_properties/CMakeLists.txt b/tests/unit_tests/check/elements/watermark/test_properties/CMakeLists.txt new file mode 100644 index 000000000..ff856c579 --- /dev/null +++ b/tests/unit_tests/check/elements/watermark/test_properties/CMakeLists.txt @@ -0,0 +1,24 @@ +# ============================================================================== +# Copyright (C) 2026 Intel Corporation +# +# SPDX-License-Identifier: MIT +# ============================================================================== + +set(TARGET_NAME "test_gvawatermark_properties") + +file(GLOB MAIN_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ) + +file(GLOB MAIN_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ) + +add_executable(${TARGET_NAME} ${MAIN_SRC} ${MAIN_HEADERS}) +target_link_libraries(${TARGET_NAME} +PRIVATE + test_common + test_utils +) + +add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) diff --git a/tests/unit_tests/check/elements/watermark/test_properties/test_watermark_properties.cpp b/tests/unit_tests/check/elements/watermark/test_properties/test_watermark_properties.cpp new file mode 100644 index 000000000..b6e923251 --- /dev/null +++ b/tests/unit_tests/check/elements/watermark/test_properties/test_watermark_properties.cpp @@ -0,0 +1,309 @@ +/******************************************************************************* + * Copyright (C) 2026 Intel Corporation + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +#include "test_common.h" +#include "test_utils.h" + +/* ---------- element names under test ---------- */ +constexpr char impl_name[] = "gvawatermarkimpl"; +constexpr char bin_name[] = "gvawatermark"; + +/* ========================================================================= */ +/* Element instantiation tests */ +/* ========================================================================= */ + +GST_START_TEST(test_watermarkimpl_instantiation) { + g_print("Starting test: test_watermarkimpl_instantiation\n"); + GstElement *element = gst_element_factory_make(impl_name, NULL); + ck_assert_msg(element != NULL, "Failed to create element '%s'", impl_name); + ck_assert(GST_IS_ELEMENT(element)); + gst_object_unref(element); +} +GST_END_TEST; + +GST_START_TEST(test_watermark_bin_instantiation) { + g_print("Starting test: test_watermark_bin_instantiation\n"); + GstElement *element = gst_element_factory_make(bin_name, NULL); + ck_assert_msg(element != NULL, "Failed to create element '%s'", bin_name); + ck_assert(GST_IS_ELEMENT(element)); + ck_assert(GST_IS_BIN(element)); + gst_object_unref(element); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Default property value tests */ +/* ========================================================================= */ + +GST_START_TEST(test_default_device_property) { + g_print("Starting test: test_default_device_property\n"); + GstElement *element = gst_check_setup_element(impl_name); + ck_assert(element != NULL); + + gchar *device = NULL; + g_object_get(G_OBJECT(element), "device", &device, NULL); + ck_assert_msg(device == NULL, "Expected default device to be NULL, got '%s'", device); + g_free(device); + + gst_check_teardown_element(element); +} +GST_END_TEST; + +GST_START_TEST(test_default_obb_property) { + g_print("Starting test: test_default_obb_property\n"); + GstElement *element = gst_check_setup_element(impl_name); + ck_assert(element != NULL); + + gboolean obb = TRUE; + g_object_get(G_OBJECT(element), "obb", &obb, NULL); + ck_assert_msg(obb == FALSE, "Expected default obb to be FALSE"); + + gst_check_teardown_element(element); +} +GST_END_TEST; + +GST_START_TEST(test_default_displ_avgfps_property) { + g_print("Starting test: test_default_displ_avgfps_property\n"); + GstElement *element = gst_check_setup_element(impl_name); + ck_assert(element != NULL); + + gboolean displ_avgfps = TRUE; + g_object_get(G_OBJECT(element), "displ-avgfps", &displ_avgfps, NULL); + ck_assert_msg(displ_avgfps == FALSE, "Expected default displ-avgfps to be FALSE"); + + gst_check_teardown_element(element); +} +GST_END_TEST; + +GST_START_TEST(test_default_displ_cfg_property) { + g_print("Starting test: test_default_displ_cfg_property\n"); + GstElement *element = gst_check_setup_element(impl_name); + ck_assert(element != NULL); + + gchar *displ_cfg = NULL; + g_object_get(G_OBJECT(element), "displ-cfg", &displ_cfg, NULL); + ck_assert_msg(displ_cfg == NULL, "Expected default displ-cfg to be NULL, got '%s'", displ_cfg); + g_free(displ_cfg); + + gst_check_teardown_element(element); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Property set/get round-trip tests */ +/* ========================================================================= */ + +GST_START_TEST(test_set_get_device_cpu) { + g_print("Starting test: test_set_get_device_cpu\n"); + GValue prop_value = G_VALUE_INIT; + g_value_init(&prop_value, G_TYPE_STRING); + g_value_set_string(&prop_value, "CPU"); + + check_property_value_updated_correctly(impl_name, "device", prop_value); + g_value_unset(&prop_value); +} +GST_END_TEST; + +GST_START_TEST(test_set_get_device_gpu) { + g_print("Starting test: test_set_get_device_gpu\n"); + GValue prop_value = G_VALUE_INIT; + g_value_init(&prop_value, G_TYPE_STRING); + g_value_set_string(&prop_value, "GPU"); + + check_property_value_updated_correctly(impl_name, "device", prop_value); + g_value_unset(&prop_value); +} +GST_END_TEST; + +GST_START_TEST(test_set_get_obb_true) { + g_print("Starting test: test_set_get_obb_true\n"); + GValue prop_value = G_VALUE_INIT; + g_value_init(&prop_value, G_TYPE_BOOLEAN); + g_value_set_boolean(&prop_value, TRUE); + + check_property_value_updated_correctly(impl_name, "obb", prop_value); + g_value_unset(&prop_value); +} +GST_END_TEST; + +GST_START_TEST(test_set_get_displ_avgfps_true) { + g_print("Starting test: test_set_get_displ_avgfps_true\n"); + GValue prop_value = G_VALUE_INIT; + g_value_init(&prop_value, G_TYPE_BOOLEAN); + g_value_set_boolean(&prop_value, TRUE); + + check_property_value_updated_correctly(impl_name, "displ-avgfps", prop_value); + g_value_unset(&prop_value); +} +GST_END_TEST; + +GST_START_TEST(test_set_get_displ_cfg) { + g_print("Starting test: test_set_get_displ_cfg\n"); + GValue prop_value = G_VALUE_INIT; + g_value_init(&prop_value, G_TYPE_STRING); + g_value_set_string(&prop_value, "show-labels=false,thickness=3"); + + check_property_value_updated_correctly(impl_name, "displ-cfg", prop_value); + g_value_unset(&prop_value); +} +GST_END_TEST; + +GST_START_TEST(test_set_get_displ_cfg_full) { + g_print("Starting test: test_set_get_displ_cfg_full\n"); + GValue prop_value = G_VALUE_INIT; + g_value_init(&prop_value, G_TYPE_STRING); + g_value_set_string(&prop_value, "show-labels=true,font-scale=1.0,thickness=5,color-idx=2,font-type=simplex"); + + check_property_value_updated_correctly(impl_name, "displ-cfg", prop_value); + g_value_unset(&prop_value); +} +GST_END_TEST; + +/* ========================================================================= */ +/* State transition tests */ +/* ========================================================================= */ + +GST_START_TEST(test_watermarkimpl_state_null_to_ready) { + g_print("Starting test: test_watermarkimpl_state_null_to_ready\n"); + GstElement *element = gst_element_factory_make(impl_name, NULL); + ck_assert(element != NULL); + + GstStateChangeReturn ret = gst_element_set_state(element, GST_STATE_READY); + ck_assert_msg(ret != GST_STATE_CHANGE_FAILURE, "Failed to transition to READY state"); + + ret = gst_element_set_state(element, GST_STATE_NULL); + ck_assert_msg(ret != GST_STATE_CHANGE_FAILURE, "Failed to transition back to NULL state"); + + gst_object_unref(element); +} +GST_END_TEST; + +GST_START_TEST(test_watermark_bin_state_null_to_ready) { + g_print("Starting test: test_watermark_bin_state_null_to_ready\n"); + GstElement *element = gst_element_factory_make(bin_name, NULL); + ck_assert(element != NULL); + + GstStateChangeReturn ret = gst_element_set_state(element, GST_STATE_READY); + ck_assert_msg(ret != GST_STATE_CHANGE_FAILURE, "Failed to transition bin to READY state"); + + ret = gst_element_set_state(element, GST_STATE_NULL); + ck_assert_msg(ret != GST_STATE_CHANGE_FAILURE, "Failed to transition bin back to NULL state"); + + gst_object_unref(element); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Bin wrapper property forwarding tests */ +/* ========================================================================= */ + +GST_START_TEST(test_bin_set_get_device) { + g_print("Starting test: test_bin_set_get_device\n"); + GstElement *element = gst_element_factory_make(bin_name, NULL); + ck_assert(element != NULL); + + g_object_set(G_OBJECT(element), "device", "CPU", NULL); + gchar *device = NULL; + g_object_get(G_OBJECT(element), "device", &device, NULL); + ck_assert_str_eq(device, "CPU"); + g_free(device); + + gst_object_unref(element); +} +GST_END_TEST; + +GST_START_TEST(test_bin_set_get_obb) { + g_print("Starting test: test_bin_set_get_obb\n"); + GstElement *element = gst_element_factory_make(bin_name, NULL); + ck_assert(element != NULL); + + g_object_set(G_OBJECT(element), "obb", TRUE, NULL); + gboolean obb = FALSE; + g_object_get(G_OBJECT(element), "obb", &obb, NULL); + ck_assert(obb == TRUE); + + gst_object_unref(element); +} +GST_END_TEST; + +GST_START_TEST(test_bin_set_get_displ_avgfps) { + g_print("Starting test: test_bin_set_get_displ_avgfps\n"); + GstElement *element = gst_element_factory_make(bin_name, NULL); + ck_assert(element != NULL); + + g_object_set(G_OBJECT(element), "displ-avgfps", TRUE, NULL); + gboolean displ_avgfps = FALSE; + g_object_get(G_OBJECT(element), "displ-avgfps", &displ_avgfps, NULL); + ck_assert(displ_avgfps == TRUE); + + gst_object_unref(element); +} +GST_END_TEST; + +GST_START_TEST(test_bin_set_get_displ_cfg) { + g_print("Starting test: test_bin_set_get_displ_cfg\n"); + GstElement *element = gst_element_factory_make(bin_name, NULL); + ck_assert(element != NULL); + + g_object_set(G_OBJECT(element), "displ-cfg", "show-labels=false", NULL); + gchar *displ_cfg = NULL; + g_object_get(G_OBJECT(element), "displ-cfg", &displ_cfg, NULL); + ck_assert_str_eq(displ_cfg, "show-labels=false"); + g_free(displ_cfg); + + gst_object_unref(element); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Suite setup */ +/* ========================================================================= */ + +static Suite *watermark_properties_testing_suite(void) { + Suite *s = suite_create("watermark_properties_testing"); + + /* instantiation */ + TCase *tc_instantiation = tcase_create("instantiation"); + suite_add_tcase(s, tc_instantiation); + tcase_add_test(tc_instantiation, test_watermarkimpl_instantiation); + tcase_add_test(tc_instantiation, test_watermark_bin_instantiation); + + /* default property values */ + TCase *tc_defaults = tcase_create("default_properties"); + suite_add_tcase(s, tc_defaults); + tcase_add_test(tc_defaults, test_default_device_property); + tcase_add_test(tc_defaults, test_default_obb_property); + tcase_add_test(tc_defaults, test_default_displ_avgfps_property); + tcase_add_test(tc_defaults, test_default_displ_cfg_property); + + /* property set/get round-trip */ + TCase *tc_setget = tcase_create("property_set_get"); + suite_add_tcase(s, tc_setget); + tcase_add_test(tc_setget, test_set_get_device_cpu); + tcase_add_test(tc_setget, test_set_get_device_gpu); + tcase_add_test(tc_setget, test_set_get_obb_true); + tcase_add_test(tc_setget, test_set_get_displ_avgfps_true); + tcase_add_test(tc_setget, test_set_get_displ_cfg); + tcase_add_test(tc_setget, test_set_get_displ_cfg_full); + + /* state transitions */ + TCase *tc_states = tcase_create("state_transitions"); + suite_add_tcase(s, tc_states); + tcase_add_test(tc_states, test_watermarkimpl_state_null_to_ready); + tcase_add_test(tc_states, test_watermark_bin_state_null_to_ready); + + /* bin property forwarding */ + TCase *tc_bin = tcase_create("bin_property_forwarding"); + suite_add_tcase(s, tc_bin); + tcase_add_test(tc_bin, test_bin_set_get_device); + tcase_add_test(tc_bin, test_bin_set_get_obb); + tcase_add_test(tc_bin, test_bin_set_get_displ_avgfps); + tcase_add_test(tc_bin, test_bin_set_get_displ_cfg); + + return s; +} + +GST_CHECK_MAIN(watermark_properties_testing); diff --git a/tests/unit_tests/check/elements/watermark/test_watermark/CMakeLists.txt b/tests/unit_tests/check/elements/watermark/test_watermark/CMakeLists.txt new file mode 100644 index 000000000..009372e97 --- /dev/null +++ b/tests/unit_tests/check/elements/watermark/test_watermark/CMakeLists.txt @@ -0,0 +1,19 @@ +# ============================================================================== +# Copyright (C) 2026 Intel Corporation +# +# SPDX-License-Identifier: MIT +# ============================================================================== + +set(COMMON_LIBS test_common test_utils) + +# --- Display config parsing tests --- +set(TARGET_CONFIG "test_gvawatermark_config") +add_executable(${TARGET_CONFIG} ${CMAKE_CURRENT_SOURCE_DIR}/test_watermark_config.cpp) +target_link_libraries(${TARGET_CONFIG} PRIVATE ${COMMON_LIBS}) +add_test(NAME ${TARGET_CONFIG} COMMAND ${TARGET_CONFIG} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + +# --- Caps negotiation tests --- +set(TARGET_CAPS "test_gvawatermark_caps") +add_executable(${TARGET_CAPS} ${CMAKE_CURRENT_SOURCE_DIR}/test_watermark_caps.cpp) +target_link_libraries(${TARGET_CAPS} PRIVATE ${COMMON_LIBS}) +add_test(NAME ${TARGET_CAPS} COMMAND ${TARGET_CAPS} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) diff --git a/tests/unit_tests/check/elements/watermark/test_watermark/test_watermark_caps.cpp b/tests/unit_tests/check/elements/watermark/test_watermark/test_watermark_caps.cpp new file mode 100644 index 000000000..cb61769b3 --- /dev/null +++ b/tests/unit_tests/check/elements/watermark/test_watermark/test_watermark_caps.cpp @@ -0,0 +1,265 @@ +/******************************************************************************* + * Copyright (C) 2026 Intel Corporation + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +#include "test_common.h" +#include "test_utils.h" +#include + +/* ---------- element name under test ---------- */ +constexpr char impl_name[] = "gvawatermarkimpl"; + +static Resolution test_resolution = {320, 240}; + +/* ========================================================================= */ +/* Data-driven caps negotiation */ +/* */ +/* A single test function iterates over all supported system-memory formats */ +/* via tcase_add_loop_test, avoiding duplicated pad template declarations. */ +/* ========================================================================= */ + +struct FormatEntry { + const char *name; /* human-readable, for messages */ + const char *caps_string; /* GST_VIDEO_CAPS_MAKE result */ +}; + +/* Constructed at file scope because GST_VIDEO_CAPS_MAKE is a macro that + expands to a string literal — safe for static init. */ +static const FormatEntry supported_formats[] = { + {"BGR", GST_VIDEO_CAPS_MAKE("BGR")}, {"NV12", GST_VIDEO_CAPS_MAKE("NV12")}, {"BGRA", GST_VIDEO_CAPS_MAKE("BGRA")}, + {"RGBA", GST_VIDEO_CAPS_MAKE("RGBA")}, {"BGRx", GST_VIDEO_CAPS_MAKE("BGRx")}, {"I420", GST_VIDEO_CAPS_MAKE("I420")}, +}; +static const int NUM_FORMATS = sizeof(supported_formats) / sizeof(supported_formats[0]); + +/* + * Build a src/sink GstStaticPadTemplate pair matching the given caps string. + * The caps_string must be a string literal or static storage (GstStaticPadTemplate + * stores the pointer, not a copy). + */ +static GstStaticPadTemplate make_src_template(const char *caps_str) { + GstStaticPadTemplate t = GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS(caps_str)); + return t; +} +static GstStaticPadTemplate make_sink_template(const char *caps_str) { + GstStaticPadTemplate t = GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS(caps_str)); + return t; +} + +GST_START_TEST(test_caps_format_accepted) { + const FormatEntry &fmt = supported_formats[__i__]; + g_print("Starting test: test_caps_format_accepted[%s]\n", fmt.name); + + GstStaticPadTemplate src = make_src_template(fmt.caps_string); + GstStaticPadTemplate sink = make_sink_template(fmt.caps_string); + + run_test(impl_name, fmt.caps_string, test_resolution, &src, &sink, NULL, NULL, NULL, NULL); +} +GST_END_TEST; + +/* ========================================================================= */ +/* GPU device with system memory should fail */ +/* ========================================================================= */ + +GST_START_TEST(test_gpu_device_with_system_memory_fails) { + g_print("Starting test: test_gpu_device_with_system_memory_fails\n"); + + GstElement *pipeline = gst_pipeline_new("test-pipeline"); + GstElement *source = gst_element_factory_make("videotestsrc", "source"); + GstElement *element = gst_element_factory_make(impl_name, "watermark"); + GstElement *sink = gst_element_factory_make("fakesink", "sink"); + + ck_assert(pipeline && source && element && sink); + + g_object_set(G_OBJECT(source), "num-buffers", 1, NULL); + g_object_set(G_OBJECT(element), "device", "GPU", NULL); + + gst_bin_add_many(GST_BIN(pipeline), source, element, sink, NULL); + gst_element_link_many(source, element, sink, NULL); + + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); + gst_element_set_state(pipeline, GST_STATE_PLAYING); + + GstMessage *msg = gst_bus_poll(bus, (GstMessageType)(GST_MESSAGE_ERROR | GST_MESSAGE_EOS), 5 * GST_SECOND); + ck_assert_msg(msg != NULL, "Expected an error message on the bus"); + ck_assert_msg(GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR, "Expected ERROR message, got %s", + GST_MESSAGE_TYPE_NAME(msg)); + + GError *err = NULL; + gst_message_parse_error(msg, &err, NULL); + ck_assert(err != NULL); + ck_assert_msg(strstr(err->message, "incompatible with System Memory") != NULL, + "Error message does not mention 'incompatible with System Memory'. Got: %s", err->message); + + g_error_free(err); + gst_message_unref(msg); + gst_object_unref(bus); + gst_element_set_state(pipeline, GST_STATE_NULL); + gst_object_unref(pipeline); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Buffer pass-through with no metadata */ +/* */ +/* Push a buffer filled with 0xAB and no ROI metadata through the element. */ +/* Verify the output buffer is byte-identical (in-place, no rendering). */ +/* ========================================================================= */ + +static void fill_buffer_with_pattern(GstBuffer *buffer, gpointer user_data) { + (void)user_data; + GstMapInfo info; + ck_assert(gst_buffer_map(buffer, &info, GST_MAP_WRITE)); + memset(info.data, 0xAB, info.size); + gst_buffer_unmap(buffer, &info); +} + +static void verify_buffer_unchanged(GstBuffer *buffer, gpointer user_data) { + (void)user_data; + GstMapInfo info; + ck_assert(gst_buffer_map(buffer, &info, GST_MAP_READ)); + + /* Fast path: single memcmp against a reference pattern */ + guint8 *reference = (guint8 *)g_malloc(info.size); + memset(reference, 0xAB, info.size); + int cmp = memcmp(info.data, reference, info.size); + if (cmp != 0) { + /* Find first mismatch for a useful diagnostic */ + for (gsize i = 0; i < info.size; i++) { + if (info.data[i] != 0xAB) { + g_free(reference); + gst_buffer_unmap(buffer, &info); + ck_abort_msg("Buffer byte %zu was modified (expected 0xAB, got 0x%02X). " + "No metadata was attached so watermark should not modify pixels.", + i, info.data[i]); + } + } + } + g_free(reference); + gst_buffer_unmap(buffer, &info); +} + +/* + * Passthrough tests use a loop over a subset of formats (BGR + NV12) + * to verify in-place identity for both packed and planar layouts. + */ +static const FormatEntry passthrough_formats[] = { + {"BGR", GST_VIDEO_CAPS_MAKE("BGR")}, + {"NV12", GST_VIDEO_CAPS_MAKE("NV12")}, +}; +static const int NUM_PASSTHROUGH_FORMATS = sizeof(passthrough_formats) / sizeof(passthrough_formats[0]); + +GST_START_TEST(test_passthrough_no_metadata) { + const FormatEntry &fmt = passthrough_formats[__i__]; + g_print("Starting test: test_passthrough_no_metadata[%s]\n", fmt.name); + + GstStaticPadTemplate src = make_src_template(fmt.caps_string); + GstStaticPadTemplate sink = make_sink_template(fmt.caps_string); + + run_test(impl_name, fmt.caps_string, test_resolution, &src, &sink, fill_buffer_with_pattern, + verify_buffer_unchanged, NULL, NULL); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Unsupported device name should fail */ +/* ========================================================================= */ + +GST_START_TEST(test_unsupported_device_name_fails) { + g_print("Starting test: test_unsupported_device_name_fails\n"); + + GstElement *pipeline = gst_pipeline_new("test-pipeline"); + GstElement *source = gst_element_factory_make("videotestsrc", "source"); + GstElement *element = gst_element_factory_make(impl_name, "watermark"); + GstElement *sink = gst_element_factory_make("fakesink", "sink"); + + ck_assert(pipeline && source && element && sink); + + g_object_set(G_OBJECT(source), "num-buffers", 1, NULL); + g_object_set(G_OBJECT(element), "device", "FPGA", NULL); + + gst_bin_add_many(GST_BIN(pipeline), source, element, sink, NULL); + gst_element_link_many(source, element, sink, NULL); + + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); + gst_element_set_state(pipeline, GST_STATE_PLAYING); + + GstMessage *msg = gst_bus_poll(bus, (GstMessageType)(GST_MESSAGE_ERROR | GST_MESSAGE_EOS), 5 * GST_SECOND); + ck_assert_msg(msg != NULL, "Expected an error message on the bus"); + ck_assert_msg(GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR, "Expected ERROR message, got %s", + GST_MESSAGE_TYPE_NAME(msg)); + + GError *err = NULL; + gchar *dbg = NULL; + gst_message_parse_error(msg, &err, &dbg); + ck_assert(err != NULL); + ck_assert_msg(strstr(err->message, "Unsupported") != NULL || (dbg && strstr(dbg, "not supported") != NULL), + "Error should mention unsupported device. Got: %s (debug: %s)", err->message, dbg ? dbg : "(null)"); + + g_error_free(err); + g_free(dbg); + gst_message_unref(msg); + gst_object_unref(bus); + gst_element_set_state(pipeline, GST_STATE_NULL); + gst_object_unref(pipeline); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Various resolutions */ +/* ========================================================================= */ + +static const Resolution resolution_variants[] = { + {64, 48}, + {1920, 1080}, +}; +static const int NUM_RESOLUTIONS = sizeof(resolution_variants) / sizeof(resolution_variants[0]); + +GST_START_TEST(test_caps_resolution_variant) { + const Resolution &res = resolution_variants[__i__]; + g_print("Starting test: test_caps_resolution_variant[%dx%d]\n", res.width, res.height); + + GstStaticPadTemplate src = make_src_template(GST_VIDEO_CAPS_MAKE("BGR")); + GstStaticPadTemplate sink = make_sink_template(GST_VIDEO_CAPS_MAKE("BGR")); + + run_test(impl_name, GST_VIDEO_CAPS_MAKE("BGR"), res, &src, &sink, NULL, NULL, NULL, NULL); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Suite setup */ +/* ========================================================================= */ + +static Suite *watermark_caps_testing_suite(void) { + Suite *s = suite_create("watermark_caps_testing"); + + /* Caps negotiation — each supported system memory format (loop test) */ + TCase *tc_caps = tcase_create("caps_negotiation"); + tcase_set_timeout(tc_caps, 30); + suite_add_tcase(s, tc_caps); + tcase_add_loop_test(tc_caps, test_caps_format_accepted, 0, NUM_FORMATS); + + /* Failure cases */ + TCase *tc_fail = tcase_create("caps_failure"); + tcase_set_timeout(tc_fail, 30); + suite_add_tcase(s, tc_fail); + tcase_add_test(tc_fail, test_gpu_device_with_system_memory_fails); + tcase_add_test(tc_fail, test_unsupported_device_name_fails); + + /* Buffer pass-through with no metadata (loop test) */ + TCase *tc_passthrough = tcase_create("buffer_passthrough"); + tcase_set_timeout(tc_passthrough, 30); + suite_add_tcase(s, tc_passthrough); + tcase_add_loop_test(tc_passthrough, test_passthrough_no_metadata, 0, NUM_PASSTHROUGH_FORMATS); + + /* Resolution variants (loop test) */ + TCase *tc_resolution = tcase_create("resolution_variants"); + tcase_set_timeout(tc_resolution, 30); + suite_add_tcase(s, tc_resolution); + tcase_add_loop_test(tc_resolution, test_caps_resolution_variant, 0, NUM_RESOLUTIONS); + + return s; +} + +GST_CHECK_MAIN(watermark_caps_testing); diff --git a/tests/unit_tests/check/elements/watermark/test_watermark/test_watermark_config.cpp b/tests/unit_tests/check/elements/watermark/test_watermark/test_watermark_config.cpp new file mode 100644 index 000000000..e4ca027bc --- /dev/null +++ b/tests/unit_tests/check/elements/watermark/test_watermark/test_watermark_config.cpp @@ -0,0 +1,421 @@ +/******************************************************************************* + * Copyright (C) 2026 Intel Corporation + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +#include "test_common.h" +#include "test_utils.h" +#include +#include +#include +#include +#include + +/* ---------- element name under test ---------- */ +constexpr char impl_name[] = "gvawatermarkimpl"; + +#define WATERMARK_BGR_CAPS GST_VIDEO_CAPS_MAKE("BGR") + +static GstStaticPadTemplate srctemplate = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS(WATERMARK_BGR_CAPS)); +static GstStaticPadTemplate sinktemplate = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS(WATERMARK_BGR_CAPS)); + +static Resolution test_resolution = {320, 240}; + +/* ========================================================================= */ +/* Helpers: attach ROI metadata and count modified pixels */ +/* ========================================================================= */ + +static void add_roi_to_buffer(GstBuffer *buffer, const char *label, guint x, guint y, guint w, guint h) { + GstAnalyticsRelationMeta *relation_meta = gst_buffer_add_analytics_relation_meta(buffer); + GstAnalyticsODMtd od_mtd; + gst_analytics_relation_meta_add_od_mtd(relation_meta, g_quark_from_string(label), (gint)x, (gint)y, (gint)w, + (gint)h, 0.95f, &od_mtd); + GstVideoRegionOfInterestMeta *roi_meta = gst_buffer_add_video_region_of_interest_meta(buffer, label, x, y, w, h); + roi_meta->id = od_mtd.id; +} + +/* Context passed through user_data for behavioral tests */ +struct RenderStats { + const char *label; /* ROI label (NULL → "person") */ + gsize modified; /* filled by check callback */ +}; + +static void setup_buffer_with_roi(GstBuffer *buffer, gpointer user_data) { + GstMapInfo info; + ck_assert(gst_buffer_map(buffer, &info, GST_MAP_WRITE)); + memset(info.data, 0x00, info.size); + gst_buffer_unmap(buffer, &info); + + const char *label = "person"; + if (user_data) { + RenderStats *stats = (RenderStats *)user_data; + if (stats->label) + label = stats->label; + } + add_roi_to_buffer(buffer, label, 50, 50, 100, 100); +} + +/* Count how many bytes in the buffer differ from 0x00 (black). + Uses branch-free increment to avoid branch mispredictions on large buffers. */ +static gsize count_modified_bytes(GstBuffer *buffer) { + GstMapInfo info; + ck_assert(gst_buffer_map(buffer, &info, GST_MAP_READ)); + gsize count = 0; + for (gsize i = 0; i < info.size; i++) { + count += (info.data[i] != 0x00); + } + gst_buffer_unmap(buffer, &info); + return count; +} + +static void record_modified_bytes(GstBuffer *buffer, gpointer user_data) { + RenderStats *stats = (RenderStats *)user_data; + stats->modified = count_modified_bytes(buffer); +} + +/* ========================================================================= */ +/* Data-driven valid config acceptance tests */ +/* */ +/* Each config string is pushed through the element via run_test. If the */ +/* config is valid, no crash or error occurs. */ +/* ========================================================================= */ + +static const char *valid_config_strings[] = { + "show-labels=false", + "font-scale=1.0", + "thickness=5", + "color-idx=0", + "font-type=simplex", + "draw-txt-bg=false", + "show-roi=person:car", + "hide-roi=background", + "show-labels=true,font-scale=0.8,thickness=3,color-idx=1,font-type=complex,draw-txt-bg=true", + "text-x=10,text-y=50", +}; +static const int NUM_VALID_CONFIGS = sizeof(valid_config_strings) / sizeof(valid_config_strings[0]); + +GST_START_TEST(test_config_accepted) { + const char *cfg = valid_config_strings[__i__]; + g_print("Starting test: test_config_accepted[%s]\n", cfg); + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, NULL, NULL, NULL, "displ-cfg", + cfg, NULL); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Test: show-labels affects rendering output */ +/* */ +/* With ROI metadata attached: */ +/* - show-labels=true: bounding box + text label drawn (more modified px) */ +/* - show-labels=false: only bounding box drawn (fewer modified px) */ +/* ========================================================================= */ + +GST_START_TEST(test_show_labels_true_renders_more_than_false) { + g_print("Starting test: test_show_labels_true_renders_more_than_false\n"); + + RenderStats with_labels = {NULL, 0}; + RenderStats without_labels = {NULL, 0}; + + /* Run with labels ON (default) */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &with_labels, NULL); + + /* Run with labels OFF */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &without_labels, "displ-cfg", "show-labels=false", NULL); + + g_print(" Modified bytes with labels: %zu, without labels: %zu\n", with_labels.modified, without_labels.modified); + + ck_assert_msg(with_labels.modified > 0, "Expected some pixels modified when ROI is present (labels on)"); + ck_assert_msg(without_labels.modified > 0, + "Expected some pixels modified when ROI is present (labels off, but bbox still drawn)"); + ck_assert_msg(with_labels.modified > without_labels.modified, + "show-labels=true should render more pixels than show-labels=false. " + "With: %zu, Without: %zu", + with_labels.modified, without_labels.modified); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Test: ROI filtering via show-roi / hide-roi */ +/* ========================================================================= */ + +GST_START_TEST(test_show_roi_filters_non_matching) { + g_print("Starting test: test_show_roi_filters_non_matching\n"); + + RenderStats unfiltered = {"person", 0}; + RenderStats filtered = {"person", 0}; + + /* No filter: ROI "person" is rendered */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &unfiltered, NULL); + + /* show-roi=car: only "car" should be shown, "person" should be filtered out */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &filtered, "displ-cfg", "show-roi=car", NULL); + + g_print(" Unfiltered: %zu bytes, Filtered (show-roi=car): %zu bytes\n", unfiltered.modified, filtered.modified); + + ck_assert_msg(unfiltered.modified > 0, "Expected pixels modified when 'person' ROI rendered without filter"); + ck_assert_msg(filtered.modified == 0, + "Expected zero pixels modified when 'person' ROI is filtered out by show-roi=car. Got: %zu", + filtered.modified); +} +GST_END_TEST; + +GST_START_TEST(test_hide_roi_hides_matching) { + g_print("Starting test: test_hide_roi_hides_matching\n"); + + RenderStats unfiltered = {"person", 0}; + RenderStats filtered = {"person", 0}; + + /* No filter: ROI "person" is rendered */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &unfiltered, NULL); + + /* hide-roi=person: "person" should be hidden */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &filtered, "displ-cfg", "hide-roi=person", NULL); + + g_print(" Unfiltered: %zu bytes, Filtered (hide-roi=person): %zu bytes\n", unfiltered.modified, filtered.modified); + + ck_assert_msg(unfiltered.modified > 0, "Expected pixels modified when 'person' ROI rendered without filter"); + ck_assert_msg(filtered.modified == 0, + "Expected zero pixels modified when 'person' ROI is hidden by hide-roi=person. Got: %zu", + filtered.modified); +} +GST_END_TEST; + +GST_START_TEST(test_show_roi_allows_matching) { + g_print("Starting test: test_show_roi_allows_matching\n"); + + RenderStats stats = {"person", 0}; + + /* show-roi=person: "person" should still be rendered */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &stats, "displ-cfg", "show-roi=person", NULL); + + ck_assert_msg(stats.modified > 0, "Expected pixels modified when 'person' ROI matches show-roi=person filter"); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Test: thickness affects rendering */ +/* ========================================================================= */ + +GST_START_TEST(test_thickness_affects_pixel_count) { + g_print("Starting test: test_thickness_affects_pixel_count\n"); + + RenderStats thin = {NULL, 0}; + RenderStats thick = {NULL, 0}; + + /* Thin bounding box (thickness=1) */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &thin, "displ-cfg", "thickness=1,show-labels=false", NULL); + + /* Thick bounding box (thickness=9) */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &thick, "displ-cfg", "thickness=9,show-labels=false", NULL); + + g_print(" Thin (1): %zu bytes, Thick (9): %zu bytes\n", thin.modified, thick.modified); + + ck_assert_msg(thick.modified > thin.modified, + "thickness=9 should produce more modified pixels than thickness=1. " + "Thin: %zu, Thick: %zu", + thin.modified, thick.modified); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Test: show-roi + hide-roi conflict (show-roi takes precedence) */ +/* ========================================================================= */ + +GST_START_TEST(test_show_roi_hide_roi_conflict) { + g_print("Starting test: test_show_roi_hide_roi_conflict\n"); + + RenderStats stats = {"person", 0}; + + /* Both show-roi=person and hide-roi=person set. + Source code: when show-roi is non-empty, hide-roi is ignored. + So "person" should still be rendered. */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &stats, "displ-cfg", "show-roi=person,hide-roi=person", NULL); + + g_print(" Modified bytes (show-roi=person,hide-roi=person): %zu\n", stats.modified); + + ck_assert_msg(stats.modified > 0, + "When both show-roi and hide-roi match, show-roi should take precedence. " + "Expected rendering, got %zu modified bytes", + stats.modified); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Test: invalid config value causes pipeline error */ +/* ========================================================================= */ + +GST_START_TEST(test_invalid_config_value_fails) { + g_print("Starting test: test_invalid_config_value_fails\n"); + + GstElement *pipeline = gst_pipeline_new("test-pipeline"); + GstElement *source = gst_element_factory_make("videotestsrc", "source"); + GstElement *element = gst_element_factory_make(impl_name, "watermark"); + GstElement *sink = gst_element_factory_make("fakesink", "sink"); + + ck_assert(pipeline && source && element && sink); + + g_object_set(G_OBJECT(source), "num-buffers", 1, NULL); + g_object_set(G_OBJECT(element), "displ-cfg", "thickness=abc", NULL); + + gst_bin_add_many(GST_BIN(pipeline), source, element, sink, NULL); + gst_element_link_many(source, element, sink, NULL); + + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); + gst_element_set_state(pipeline, GST_STATE_PLAYING); + + GstMessage *msg = gst_bus_poll(bus, (GstMessageType)(GST_MESSAGE_ERROR | GST_MESSAGE_EOS), 5 * GST_SECOND); + ck_assert_msg(msg != NULL, "Expected an error message on the bus"); + ck_assert_msg(GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR, "Expected ERROR message, got %s", + GST_MESSAGE_TYPE_NAME(msg)); + + GError *err = NULL; + gst_message_parse_error(msg, &err, NULL); + ck_assert(err != NULL); + /* set_caps catches the Impl constructor exception and posts "Could not initialize" */ + ck_assert_msg(strstr(err->message, "Could not initialize") != NULL, + "Expected 'Could not initialize' error. Got: %s", err->message); + + g_error_free(err); + gst_message_unref(msg); + gst_object_unref(bus); + gst_element_set_state(pipeline, GST_STATE_NULL); + gst_object_unref(pipeline); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Test: multiple ROIs produce more pixels than a single ROI */ +/* ========================================================================= */ + +static void setup_buffer_with_two_rois(GstBuffer *buffer, gpointer user_data) { + GstMapInfo info; + ck_assert(gst_buffer_map(buffer, &info, GST_MAP_WRITE)); + memset(info.data, 0x00, info.size); + gst_buffer_unmap(buffer, &info); + + (void)user_data; + add_roi_to_buffer(buffer, "person", 10, 10, 80, 80); + add_roi_to_buffer(buffer, "car", 200, 100, 100, 80); +} + +GST_START_TEST(test_multiple_rois_more_pixels) { + g_print("Starting test: test_multiple_rois_more_pixels\n"); + + RenderStats single = {NULL, 0}; + RenderStats multi = {NULL, 0}; + + /* Single ROI */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &single, "displ-cfg", "show-labels=false", NULL); + + /* Two ROIs */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_two_rois, + record_modified_bytes, &multi, "displ-cfg", "show-labels=false", NULL); + + g_print(" Single ROI: %zu bytes, Two ROIs: %zu bytes\n", single.modified, multi.modified); + + ck_assert_msg(single.modified > 0, "Expected pixels from single ROI"); + ck_assert_msg(multi.modified > single.modified, + "Two ROIs should produce more modified pixels than one. " + "Single: %zu, Multi: %zu", + single.modified, multi.modified); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Test: show-labels=false causes show-roi to be ignored */ +/* */ +/* In parse_displ_config, show-roi and hide-roi are parsed inside the */ +/* `if (_displCfg.show_labels)` block. So when show-labels=false, the */ +/* ROI filter is never activated — all ROIs should render their bbox. */ +/* ========================================================================= */ + +GST_START_TEST(test_show_labels_false_ignores_show_roi) { + g_print("Starting test: test_show_labels_false_ignores_show_roi\n"); + + RenderStats with_filter = {"person", 0}; + RenderStats without_filter = {"person", 0}; + + /* show-labels=true,show-roi=car → "person" ROI filtered out (0 pixels) */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &with_filter, "displ-cfg", "show-labels=true,show-roi=car", NULL); + + /* show-labels=false,show-roi=car → show-roi NOT parsed, "person" bbox still drawn */ + run_test(impl_name, WATERMARK_BGR_CAPS, test_resolution, &srctemplate, &sinktemplate, setup_buffer_with_roi, + record_modified_bytes, &without_filter, "displ-cfg", "show-labels=false,show-roi=car", NULL); + + g_print(" labels=true,show-roi=car: %zu bytes; labels=false,show-roi=car: %zu bytes\n", with_filter.modified, + without_filter.modified); + + ck_assert_msg(with_filter.modified == 0, + "With show-labels=true,show-roi=car, 'person' should be filtered. Got: %zu", with_filter.modified); + ck_assert_msg(without_filter.modified > 0, + "With show-labels=false, show-roi should be ignored — 'person' bbox should render. Got: %zu", + without_filter.modified); +} +GST_END_TEST; + +/* ========================================================================= */ +/* Suite setup */ +/* ========================================================================= */ + +static Suite *watermark_config_testing_suite(void) { + Suite *s = suite_create("watermark_config_testing"); + + /* Valid config strings accepted (loop test) */ + TCase *tc_valid = tcase_create("valid_configs"); + tcase_set_timeout(tc_valid, 30); + suite_add_tcase(s, tc_valid); + tcase_add_loop_test(tc_valid, test_config_accepted, 0, NUM_VALID_CONFIGS); + + /* Behavioral: show-labels affects rendering */ + TCase *tc_labels = tcase_create("show_labels_behavior"); + tcase_set_timeout(tc_labels, 30); + suite_add_tcase(s, tc_labels); + tcase_add_test(tc_labels, test_show_labels_true_renders_more_than_false); + + /* Behavioral: ROI filtering */ + TCase *tc_filter = tcase_create("roi_filtering"); + tcase_set_timeout(tc_filter, 30); + suite_add_tcase(s, tc_filter); + tcase_add_test(tc_filter, test_show_roi_filters_non_matching); + tcase_add_test(tc_filter, test_hide_roi_hides_matching); + tcase_add_test(tc_filter, test_show_roi_allows_matching); + tcase_add_test(tc_filter, test_show_roi_hide_roi_conflict); + tcase_add_test(tc_filter, test_show_labels_false_ignores_show_roi); + + /* Error cases */ + TCase *tc_errors = tcase_create("config_errors"); + tcase_set_timeout(tc_errors, 30); + suite_add_tcase(s, tc_errors); + tcase_add_test(tc_errors, test_invalid_config_value_fails); + + /* Behavioral: multiple ROIs */ + TCase *tc_multi = tcase_create("multiple_rois"); + tcase_set_timeout(tc_multi, 30); + suite_add_tcase(s, tc_multi); + tcase_add_test(tc_multi, test_multiple_rois_more_pixels); + + /* Behavioral: thickness affects rendering */ + TCase *tc_thickness = tcase_create("thickness_behavior"); + tcase_set_timeout(tc_thickness, 30); + suite_add_tcase(s, tc_thickness); + tcase_add_test(tc_thickness, test_thickness_affects_pixel_count); + + return s; +} + +GST_CHECK_MAIN(watermark_config_testing);