Skip to content
22 changes: 21 additions & 1 deletion scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,31 @@ $COVERAGE && MESON_ARGS+=("-Db_coverage=true")
meson setup build "${MESON_ARGS[@]}" >/dev/null
ninja -C build >/dev/null

# ── Detect a live X display for opt-in screen-capture (x11grab) tests ───────
# The screen-capture success tests run only when DVLED_TEST_DISPLAY is set to
# an x11grab source string ("<display>+<x>,<y>"); otherwise they self-skip.
if [[ -z "${DVLED_TEST_DISPLAY:-}" ]] && command -v xdpyinfo &>/dev/null; then
for disp in "${DISPLAY:-}" ":99" ":0"; do
[[ -z "$disp" ]] && continue
if DISPLAY="$disp" xdpyinfo &>/dev/null; then
# Append screen ".0" only when the display has no screen already
# (e.g. ":0" → ":0.0", but leave ":0.0" untouched to avoid ":0.0.0").
disp_tail="${disp##*:}"
if [[ "$disp_tail" == *.* ]]; then
export DVLED_TEST_DISPLAY="${disp}+0,0"
else
export DVLED_TEST_DISPLAY="${disp}.0+0,0"
fi
echo "Live X display detected on $disp — enabling screen-capture tests."
break
fi
done
fi

# ── Run tests ──────────────────────────────────────────────────────────────
echo ""
echo "Running unit tests..."
echo "──────────────────────────────────────────────────────────────────────"

set +e
meson test -C build --print-errorlogs 2>&1
TEST_EXIT=$?
Expand Down
23 changes: 23 additions & 0 deletions tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,27 @@ if mtl_dep.found()
install: false
)
test('mtl_tx', test_mtl_tx)

# Stub-based tests: MTL library entry points are wrapped so the session
# lifecycle and frame-send paths run with no real MTL/DPDK hardware.
test_mtl_tx_stub = executable('test_mtl_tx_stub',
sources: [
'test_mtl_tx_stub.c',
'../src/mtl/mtl_tx.c',
'../src/util/logger.c',
],
include_directories: [test_inc, include_directories('/usr/local/include/mtl')],
dependencies: test_deps + [mtl_dep],
c_args: test_c_args + ['-DENABLE_MTL_TX'],
link_args: ['-Wl,--wrap=mtl_init',
'-Wl,--wrap=mtl_uninit',
'-Wl,--wrap=mtl_pmd_by_port_name',
'-Wl,--wrap=st20p_tx_create',
'-Wl,--wrap=st20p_tx_free',
'-Wl,--wrap=st20p_tx_frame_size',
'-Wl,--wrap=st20p_tx_get_frame',
'-Wl,--wrap=st20p_tx_put_frame'],
install: false
)
test('mtl_tx_stub', test_mtl_tx_stub)
endif
185 changes: 185 additions & 0 deletions tests/test_config_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,112 @@ static void test_validate_no_scale_passes(void **state)
dvledtx_config_free(&cfg);
}

static void test_validate_nic_index_out_of_range_fails(void **state)
{
(void)state;
struct dvledtx_config cfg;
fill_valid_config(&cfg);
cfg.sessions[0].nic_index = 5; /* only 1 NIC parsed → out of range */
assert_int_equal(validate_tx_config(&cfg), -1);
dvledtx_config_free(&cfg);
}

static void test_validate_crop_nonpositive_fails(void **state)
{
(void)state;
struct dvledtx_config cfg;
fill_valid_config(&cfg);
cfg.sessions[0].crop_w = 0; /* width must be > 0 */
assert_int_equal(validate_tx_config(&cfg), -1);
dvledtx_config_free(&cfg);
}

static void test_validate_payload_type_out_of_range_fails(void **state)
{
(void)state;
struct dvledtx_config cfg;
fill_valid_config(&cfg);
cfg.sessions[0].payload_type = 50; /* outside dynamic range 96-127 */
assert_int_equal(validate_tx_config(&cfg), -1);
dvledtx_config_free(&cfg);
}

static void test_validate_crop_y_misaligned_yuv420_fails(void **state)
{
(void)state;
struct dvledtx_config cfg;
fill_valid_config(&cfg);
strncpy(cfg.fmt, "yuv420", sizeof(cfg.fmt) - 1); /* y_align=2 */
cfg.sessions[0].crop_y = 1; /* odd → not a multiple of 2 */
cfg.sessions[0].crop_h = 1078; /* even, crop_y+crop_h=1079 < 1080 */
assert_int_equal(validate_tx_config(&cfg), -1);
dvledtx_config_free(&cfg);
}

static void test_validate_crop_h_misaligned_yuv420_fails(void **state)
{
(void)state;
struct dvledtx_config cfg;
fill_valid_config(&cfg);
strncpy(cfg.fmt, "yuv420", sizeof(cfg.fmt) - 1); /* y_align=2 */
cfg.sessions[0].crop_y = 0;
cfg.sessions[0].crop_h = 1077; /* odd → not a multiple of 2 */
assert_int_equal(validate_tx_config(&cfg), -1);
dvledtx_config_free(&cfg);
}

/* ==========================================================================
* resolve_ip_addrs tests
* ========================================================================== */

static void test_resolve_ip_addrs_valid(void **state)
{
(void)state;
struct dvledtx_context ctx;
memset(&ctx, 0, sizeof(ctx));
dvledtx_context_alloc(&ctx, 1, 1);
strncpy(ctx.nics[0].sip_addr_str, "192.168.50.29", sizeof(ctx.nics[0].sip_addr_str) - 1);
strncpy(ctx.nics[0].dip_addr_str, "239.168.85.20", sizeof(ctx.nics[0].dip_addr_str) - 1);
assert_int_equal(resolve_ip_addrs(&ctx), 0);
dvledtx_context_free(&ctx);
}

static void test_resolve_ip_addrs_empty_sip_dhcp(void **state)
{
(void)state;
struct dvledtx_context ctx;
memset(&ctx, 0, sizeof(ctx));
dvledtx_context_alloc(&ctx, 1, 1);
ctx.nics[0].sip_addr_str[0] = '\0'; /* empty → DHCP mode */
strncpy(ctx.nics[0].dip_addr_str, "239.168.85.20", sizeof(ctx.nics[0].dip_addr_str) - 1);
assert_int_equal(resolve_ip_addrs(&ctx), 0);
dvledtx_context_free(&ctx);
}

static void test_resolve_ip_addrs_invalid_sip_fails(void **state)
{
(void)state;
struct dvledtx_context ctx;
memset(&ctx, 0, sizeof(ctx));
dvledtx_context_alloc(&ctx, 1, 1);
strncpy(ctx.nics[0].sip_addr_str, "999.1.1.1", sizeof(ctx.nics[0].sip_addr_str) - 1);
strncpy(ctx.nics[0].dip_addr_str, "239.168.85.20", sizeof(ctx.nics[0].dip_addr_str) - 1);
assert_int_equal(resolve_ip_addrs(&ctx), -1);
dvledtx_context_free(&ctx);
}

static void test_resolve_ip_addrs_invalid_dip_fails(void **state)
{
(void)state;
struct dvledtx_context ctx;
memset(&ctx, 0, sizeof(ctx));
dvledtx_context_alloc(&ctx, 1, 1);
strncpy(ctx.nics[0].sip_addr_str, "192.168.50.29", sizeof(ctx.nics[0].sip_addr_str) - 1);
strncpy(ctx.nics[0].dip_addr_str, "not-an-ip", sizeof(ctx.nics[0].dip_addr_str) - 1);
assert_int_equal(resolve_ip_addrs(&ctx), -1);
dvledtx_context_free(&ctx);
}

static void test_validate_unknown_input_mode_fails(void **state)
{
(void)state;
Expand Down Expand Up @@ -1520,6 +1626,72 @@ static void test_load_and_apply_config_unknown_fmt_fails(void **state)
assert_int_equal(ret, -1);
}

static void test_load_and_apply_config_fmt_yuv422p12le(void **state)
{
(void)state;
char *path = write_tmpfile(ONE_SESSION_JSON("yuv422p12le"));
assert_non_null(path);
struct dvledtx_context app;
memset(&app, 0, sizeof(app));
int ret = load_and_apply_config(&app, path);
unlink(path); free(path);
assert_int_equal(ret, 0);
assert_int_equal(app.fmt, AV_PIX_FMT_YUV422P12LE);
dvledtx_context_free(&app);
}

static void test_load_and_apply_config_fmt_yuv444p12le(void **state)
{
(void)state;
char *path = write_tmpfile(ONE_SESSION_JSON("yuv444p12le"));
assert_non_null(path);
struct dvledtx_context app;
memset(&app, 0, sizeof(app));
int ret = load_and_apply_config(&app, path);
unlink(path); free(path);
assert_int_equal(ret, 0);
assert_int_equal(app.fmt, AV_PIX_FMT_YUV444P12LE);
dvledtx_context_free(&app);
}

static void test_load_and_apply_config_fmt_gbrp12le(void **state)
{
(void)state;
char *path = write_tmpfile(ONE_SESSION_JSON("gbrp12le"));
assert_non_null(path);
struct dvledtx_context app;
memset(&app, 0, sizeof(app));
int ret = load_and_apply_config(&app, path);
unlink(path); free(path);
assert_int_equal(ret, 0);
assert_int_equal(app.fmt, AV_PIX_FMT_GBRP12LE);
dvledtx_context_free(&app);
}

/* Config with scaling set exercises the scale_width/scale_height copy and
* the scaling-specific LOG_INFO branch in load_and_apply_config. */
static void test_load_and_apply_config_with_scaling(void **state)
{
(void)state;
char *path = write_tmpfile(
"{"
" \"interfaces\": [{\"name\":\"0000:06:00.0\",\"sip\":\"192.168.50.29\",\"dip\":\"239.168.85.20\"}],"
" \"video\": {\"width\":1920,\"height\":1080},"
" \"tx_video\": {\"scale_width\":3840,\"scale_height\":2160,\"fps\":30,\"fmt\":\"yuv422p10le\"},"
" \"tx_sessions\": [{\"udp_port\":20000,\"payload_type\":96,"
" \"crop\":{\"x\":0,\"y\":0,\"w\":3840,\"h\":2160}}]"
"}");
assert_non_null(path);
struct dvledtx_context app;
memset(&app, 0, sizeof(app));
int ret = load_and_apply_config(&app, path);
unlink(path); free(path);
assert_int_equal(ret, 0);
assert_int_equal((int)app.scale_width, 3840);
assert_int_equal((int)app.scale_height, 2160);
dvledtx_context_free(&app);
}

static void test_load_and_apply_config_copies_log_file(void **state)
{
(void)state;
Expand Down Expand Up @@ -1636,6 +1808,15 @@ int main(void)
cmocka_unit_test(test_validate_scale_crop_exceeds_scaled_dims_fails),
cmocka_unit_test(test_validate_scale_crop_within_scaled_dims_passes),
cmocka_unit_test(test_validate_no_scale_passes),
cmocka_unit_test(test_validate_nic_index_out_of_range_fails),
cmocka_unit_test(test_validate_crop_nonpositive_fails),
cmocka_unit_test(test_validate_payload_type_out_of_range_fails),
cmocka_unit_test(test_validate_crop_y_misaligned_yuv420_fails),
cmocka_unit_test(test_validate_crop_h_misaligned_yuv420_fails),
cmocka_unit_test(test_resolve_ip_addrs_valid),
cmocka_unit_test(test_resolve_ip_addrs_empty_sip_dhcp),
cmocka_unit_test(test_resolve_ip_addrs_invalid_sip_fails),
cmocka_unit_test(test_resolve_ip_addrs_invalid_dip_fails),
cmocka_unit_test(test_validate_unknown_input_mode_fails),
cmocka_unit_test(test_validate_screen_capture_without_screen_input_fails),
cmocka_unit_test(test_validate_screen_capture_with_screen_input_passes),
Expand Down Expand Up @@ -1682,6 +1863,10 @@ int main(void)
cmocka_unit_test(test_load_and_apply_config_fmt_yuv444p),
cmocka_unit_test(test_load_and_apply_config_fmt_gbrp10le),
cmocka_unit_test(test_load_and_apply_config_fmt_yuv420),
cmocka_unit_test(test_load_and_apply_config_fmt_yuv422p12le),
cmocka_unit_test(test_load_and_apply_config_fmt_yuv444p12le),
cmocka_unit_test(test_load_and_apply_config_fmt_gbrp12le),
cmocka_unit_test(test_load_and_apply_config_with_scaling),
cmocka_unit_test(test_load_and_apply_config_unknown_fmt_fails),
cmocka_unit_test(test_load_and_apply_config_copies_log_file),
cmocka_unit_test(test_load_and_apply_config_maps_screen_capture_fields),
Expand Down
80 changes: 80 additions & 0 deletions tests/test_ffmpeg_decoder_mock.c
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,84 @@ static void test_screen_capture_per_session_invalid_display_fails_gracefully(voi
assert_null(ctx.fmt_ctx);
}

/* =========================================================================
* Test: screen capture (x11grab) SUCCESS path against a live X display
*
* The failure-path tests above only exercise the error branches. These
* tests open a real x11grab source and (for the per-session variant)
* decode a real frame, covering the success branch of open_ffmpeg_decoder()
* that is otherwise only reached by the file-based decoder tests.
*
* They require a live X server. To keep headless CI green, they are opt-in:
* they run only when DVLED_TEST_DISPLAY is set (e.g. ":99.0+0,0"), and are
* reported as skipped otherwise. scripts/test.sh sets this automatically
* when it detects a working display.
* ========================================================================= */

static void test_screen_capture_shared_success_opens_source(void **state)
{
(void)state;
const char* disp = getenv("DVLED_TEST_DISPLAY");
if (disp == NULL || disp[0] == '\0') {
skip(); /* no live X display in this environment */
}

avdevice_register_all(); /* ensure x11grab is resolvable */

struct dvledtx_context app;
fill_app_16x16(&app, 1);
app.use_screen_capture = true;
snprintf(app.screen_input, sizeof(app.screen_input), "%s", disp);

struct shared_decode_ctx dec;
memset(&dec, 0, sizeof(dec));
dec.app = &app;
dec.num_sessions = 1;

int ret = open_shared_ffmpeg(&dec, "");
assert_int_equal(ret, 0);
assert_non_null(dec.fmt_ctx);
assert_non_null(dec.codec_ctx);
assert_non_null(dec.sws_ctx);

close_shared_ffmpeg(&dec);
dvledtx_context_free(&app);
}

static void test_screen_capture_per_session_success_captures_frame(void **state)
{
(void)state;
const char* disp = getenv("DVLED_TEST_DISPLAY");
if (disp == NULL || disp[0] == '\0') {
skip(); /* no live X display in this environment */
}

avdevice_register_all(); /* ensure x11grab is resolvable */

struct dvledtx_context app;
fill_app_16x16(&app, 1);
app.use_screen_capture = true;
snprintf(app.screen_input, sizeof(app.screen_input), "%s", disp);

struct st20p_tx_ctx ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.idx = 0;
ctx.app = &app;

int ret = load_video_source(&ctx, app.screen_input);
assert_int_equal(ret, 0);
assert_true(ctx.use_ffmpeg);
assert_non_null(ctx.fmt_ctx);

/* Capture and colour-convert one real frame from the X display. */
bool got = ffmpeg_decode_next_frame(&ctx);
assert_true(got);
assert_non_null(ctx.yuv_frame);

close_ffmpeg_source(&ctx);
dvledtx_context_free(&app);
}

/* =========================================================================
* Test: shared_decode_thread — runs with generated video + barriers
* ========================================================================= */
Expand Down Expand Up @@ -513,6 +591,8 @@ int main(void)
cmocka_unit_test(test_screen_capture_after_avdevice_register_finds_x11grab),
cmocka_unit_test(test_screen_capture_shared_invalid_display_fails_gracefully),
cmocka_unit_test(test_screen_capture_per_session_invalid_display_fails_gracefully),
cmocka_unit_test(test_screen_capture_shared_success_opens_source),
cmocka_unit_test(test_screen_capture_per_session_success_captures_frame),

/* shared_decode_thread */
cmocka_unit_test(test_shared_decode_thread_decodes_frames),
Expand Down
Loading
Loading