From d1cda2b7c42e8d5418b12fbbd85a86a2e605b7ce Mon Sep 17 00:00:00 2001 From: Roshan Kumar Date: Mon, 13 Jul 2026 12:02:00 +0530 Subject: [PATCH 1/6] Ci/add codeql workflow (#1) * ci: add CodeQL Advanced workflow for C/C++ analysis Adds .github/workflows/codeql.yml, adapted from GitHub's default CodeQL Advanced setup template for this repo: - reduced language matrix to c-cpp (only language used in this repo) - build-mode set to manual, reusing the environment-check and build-dvledtx composite actions so CodeQL traces the same meson/ninja build the rest of CI performs - pinned actions/checkout and github/codeql-action to the same SHAs already used elsewhere in this repo's workflows - added concurrency group and job timeout consistent with other workflows in this repo * ci: align CodeQL workflow with Coverity pattern - codeql.yml: simplified to match coverity.yml (weekly schedule + workflow_dispatch only, single job, minimal permissions), dropping the push/pull_request triggers and language matrix since this repo only analyzes c-cpp. - scan_on_demand.yml: added CodeQL init/build/analyze steps alongside the existing Coverity Scan step, so an on-demand scan also runs CodeQL. * ci: extract CodeQL init/build/analyze into a reusable composite action Adds .github/actions/analysis/codeql/action.yml (Initialize CodeQL, Build dvledtx, Perform CodeQL Analysis), matching the pattern used by the other .github/actions/analysis/* actions (coverity, trivy, etc). codeql.yml and scan_on_demand.yml now both call this single composite action instead of duplicating the three inline steps. --- .github/actions/analysis/codeql/action.yml | 27 +++++++++++ .github/workflows/codeql.yml | 53 ++++++++++++++++++++++ .github/workflows/scan_on_demand.yml | 3 ++ 3 files changed, 83 insertions(+) create mode 100644 .github/actions/analysis/codeql/action.yml create mode 100644 .github/workflows/codeql.yml diff --git a/.github/actions/analysis/codeql/action.yml b/.github/actions/analysis/codeql/action.yml new file mode 100644 index 0000000..1296205 --- /dev/null +++ b/.github/actions/analysis/codeql/action.yml @@ -0,0 +1,27 @@ +# +# BSD 3-Clause License +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +name: 'CodeQL Scan' +description: 'Initialize CodeQL, build dvledtx, and perform CodeQL analysis for C/C++' + +runs: + using: composite + steps: + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + languages: c-cpp + build-mode: manual + + # `c-cpp` always requires a manual build so CodeQL can trace the + # compiler invocations produced by meson/ninja. + - name: Build dvledtx + uses: ./.github/actions/build-dvledtx + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + category: "/language:c-cpp" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..58d9afd --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,53 @@ +# +# BSD 3-Clause License +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# CodeQL Advanced workflow for dvledtx (C/C++ project built with meson/ninja). +# +# This repository only contains C sources, so the language matrix has been +# reduced to `c-cpp` and the build mode set to `manual`, reusing the existing +# environment-check/build-dvledtx composite actions so CodeQL traces the same +# build the rest of CI performs. +name: CodeQL Scan + +on: + schedule: + # Weekly on Monday at 07:19 UTC + - cron: '19 7 * * 1' + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + codeql: + name: CodeQL Scan + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + security-events: write # Required to upload SARIF results to GitHub Security tab + + steps: + - name: Clean up previous run + env: + WORKSPACE: ${{ github.workspace }} + run: | + find "$WORKSPACE" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Environment check + uses: ./.github/actions/environment-check + + - name: CodeQL Scan + uses: ./.github/actions/analysis/codeql diff --git a/.github/workflows/scan_on_demand.yml b/.github/workflows/scan_on_demand.yml index 3214385..7e523ee 100644 --- a/.github/workflows/scan_on_demand.yml +++ b/.github/workflows/scan_on_demand.yml @@ -114,6 +114,9 @@ jobs: - name: Trivy Scan uses: ./.github/actions/analysis/trivy + - name: CodeQL Scan + uses: ./.github/actions/analysis/codeql + - name: Coverity Scan uses: ./.github/actions/analysis/coverity env: From 0fb649675672eacd268ae03ee5b72c20edd435f1 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 21 Jul 2026 11:56:53 +0530 Subject: [PATCH 2/6] test: fix tx test segfault and raise coverage; add screen-capture success tests - Fix test_open_ffmpeg_tx_uses_app_defaults OOB (idx=1 -> idx=0) after the exactly-sized session_net refactor - Add mtl_tx get_input_format and guard/error-path tests (33% -> 53%) - Add ffmpeg_tx multi-NIC and raw-YUV send tests (68% -> 72%) - Add config_reader 12-bit format and scaling tests - Add opt-in x11grab screen-capture success tests (shared + per-session) that self-skip unless DVLED_TEST_DISPLAY is set; test.sh auto-detects a live X display --- scripts/test.sh | 15 ++- tests/test_config_reader.c | 70 ++++++++++++++ tests/test_ffmpeg_decoder_mock.c | 80 ++++++++++++++++ tests/test_ffmpeg_tx_mock.c | 105 ++++++++++++++++++++- tests/test_mtl_tx.c | 151 +++++++++++++++++++++++++++++++ 5 files changed, 417 insertions(+), 4 deletions(-) diff --git a/scripts/test.sh b/scripts/test.sh index 623840d..6c2eb99 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -46,11 +46,24 @@ $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 ("+,"); 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 + export DVLED_TEST_DISPLAY="${disp}.0+0,0" + 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=$? diff --git a/tests/test_config_reader.c b/tests/test_config_reader.c index 8d248ab..9800f67 100644 --- a/tests/test_config_reader.c +++ b/tests/test_config_reader.c @@ -1430,6 +1430,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; @@ -1588,6 +1654,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), diff --git a/tests/test_ffmpeg_decoder_mock.c b/tests/test_ffmpeg_decoder_mock.c index c89946b..45bcce9 100644 --- a/tests/test_ffmpeg_decoder_mock.c +++ b/tests/test_ffmpeg_decoder_mock.c @@ -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 * ========================================================================= */ @@ -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), diff --git a/tests/test_ffmpeg_tx_mock.c b/tests/test_ffmpeg_tx_mock.c index 3eebcb1..7537efc 100644 --- a/tests/test_ffmpeg_tx_mock.c +++ b/tests/test_ffmpeg_tx_mock.c @@ -216,13 +216,13 @@ static void test_open_ffmpeg_tx_uses_app_defaults(void **state) struct dvledtx_context app; fill_app_16x16(&app, 1); - /* Clear per-session network config — open_ffmpeg_tx must fall back to - * app-level width/height. */ + /* Clear per-session network config — with udp_port/payload_type zeroed, + * open_ffmpeg_tx must fall back to the app-level defaults. */ memset(app.session_net, 0, (size_t)app.st20p_sessions * sizeof(*app.session_net)); struct st20p_tx_ctx ctx; memset(&ctx, 0, sizeof(ctx)); - ctx.idx = 1; /* out of range → uses app defaults */ + ctx.idx = 0; /* zeroed session_net → falls back to app defaults */ ctx.app = &app; ctx.crop_width = 16; ctx.crop_height = 16; @@ -382,6 +382,100 @@ static void test_ffmpeg_tx_send_yuv_frame_pipeline(void **state) close_ffmpeg_tx(&ctx); } +/* ========================================================================= + * open_ffmpeg_tx — multi-NIC redundant-port registration (idx 0, nic_count 2) + * ========================================================================= */ + +static void test_open_ffmpeg_tx_multi_nic(void **state) +{ + (void)state; + avdevice_register_all(); + + struct dvledtx_context app; + memset(&app, 0, sizeof(app)); + assert_int_equal(dvledtx_context_alloc(&app, 2, 1), 0); + + strncpy(app.nics[0].port, "0000:06:00.0", sizeof(app.nics[0].port) - 1); + strncpy(app.nics[0].sip_addr_str, "192.168.50.29", sizeof(app.nics[0].sip_addr_str) - 1); + strncpy(app.nics[0].dip_addr_str, "239.168.85.20", sizeof(app.nics[0].dip_addr_str) - 1); + strncpy(app.nics[1].port, "0000:06:00.2", sizeof(app.nics[1].port) - 1); + strncpy(app.nics[1].sip_addr_str, "192.168.50.30", sizeof(app.nics[1].sip_addr_str) - 1); + strncpy(app.nics[1].dip_addr_str, "239.168.85.21", sizeof(app.nics[1].dip_addr_str) - 1); + app.width = 16; + app.height = 16; + app.fps = 25; + app.fmt = AV_PIX_FMT_YUV422P10LE; + app.udp_port = 20000; + app.payload_type = 96; + app.session_net[0].udp_port = 20000; + app.session_net[0].payload_type = 96; + app.session_net[0].crop_w = 16; + app.session_net[0].crop_h = 16; + app.session_net[0].nic_index = 0; + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.idx = 0; /* first session → registers second NIC as redundant */ + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + + assert_int_equal(open_ffmpeg_tx(&ctx), 0); + assert_non_null(ctx.out_fmt_ctx); + + close_ffmpeg_tx(&ctx); + dvledtx_context_free(&app); +} + +/* ========================================================================= + * ffmpeg_tx_send_raw_yuv — guard + full raw playback path (with wrap) + * ========================================================================= */ + +static void test_ffmpeg_tx_send_raw_yuv_guard(void **state) +{ + (void)state; + /* enc_frame/enc_pkt NULL → early -1 without touching MTL */ + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + assert_int_equal(ffmpeg_tx_send_raw_yuv(&ctx), -1); +} + +static void test_ffmpeg_tx_send_raw_yuv_pipeline(void **state) +{ + (void)state; + avdevice_register_all(); + + struct dvledtx_context app; + fill_app_16x16(&app, 1); + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.idx = 0; + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + + assert_int_equal(open_ffmpeg_tx(&ctx), 0); + + int frame_sz = av_image_get_buffer_size(app.fmt, 16, 16, 1); + assert_true(frame_sz > 0); + ctx.frame_size = (size_t)frame_sz; + ctx.source_size = (size_t)frame_sz * 2; /* room for two frames */ + ctx.source_buffer = calloc(1, ctx.source_size); + assert_non_null(ctx.source_buffer); + + /* First send from offset 0 */ + (void)ffmpeg_tx_send_raw_yuv(&ctx); + + /* Force the loop-playback wrap branch (current_pos + frame > source_size) */ + ctx.current_pos = ctx.source_size; + (void)ffmpeg_tx_send_raw_yuv(&ctx); + + free(ctx.source_buffer); + close_ffmpeg_tx(&ctx); + dvledtx_context_free(&app); +} + /* ========================================================================= * main * ========================================================================= */ @@ -400,6 +494,7 @@ int main(void) cmocka_unit_test(test_open_ffmpeg_tx_success), cmocka_unit_test(test_open_ffmpeg_tx_uses_app_defaults), cmocka_unit_test(test_open_ffmpeg_tx_crop_fallback), + cmocka_unit_test(test_open_ffmpeg_tx_multi_nic), /* close_ffmpeg_tx */ cmocka_unit_test(test_close_ffmpeg_tx_full), @@ -410,6 +505,10 @@ int main(void) /* pipeline: decode + tx_send_yuv_frame */ cmocka_unit_test(test_ffmpeg_tx_send_yuv_frame_pipeline), + + /* raw YUV send path */ + cmocka_unit_test(test_ffmpeg_tx_send_raw_yuv_guard), + cmocka_unit_test(test_ffmpeg_tx_send_raw_yuv_pipeline), }; int result = cmocka_run_group_tests(tests, NULL, NULL); diff --git a/tests/test_mtl_tx.c b/tests/test_mtl_tx.c index 71d64dd..b6408b1 100644 --- a/tests/test_mtl_tx.c +++ b/tests/test_mtl_tx.c @@ -36,6 +36,8 @@ #include #include +#include "app_context.h" +#include "core/session_manager.h" #include "mtl/mtl_tx.h" #include "util/logger.h" @@ -124,6 +126,44 @@ static void test_get_input_format_gbrp12le(void **state) ST_FRAME_FMT_GBRPLANAR12LE); } +/* ========================================================================= + * get_input_format — 10-bit / 8-bit formats + unknown + * ========================================================================= */ + +static void test_get_input_format_yuv422p10le(void **state) +{ + (void)state; + assert_int_equal(get_input_format(AV_PIX_FMT_YUV422P10LE), + ST_FRAME_FMT_YUV422PLANAR10LE); +} + +static void test_get_input_format_yuv420p(void **state) +{ + (void)state; + assert_int_equal(get_input_format(AV_PIX_FMT_YUV420P), + ST_FRAME_FMT_YUV420CUSTOM8); +} + +static void test_get_input_format_yuv444p10le(void **state) +{ + (void)state; + assert_int_equal(get_input_format(AV_PIX_FMT_YUV444P10LE), + ST_FRAME_FMT_YUV444PLANAR10LE); +} + +static void test_get_input_format_gbrp10le(void **state) +{ + (void)state; + assert_int_equal(get_input_format(AV_PIX_FMT_GBRP10LE), + ST_FRAME_FMT_GBRPLANAR10LE); +} + +static void test_get_input_format_unknown_returns_error(void **state) +{ + (void)state; + assert_int_equal((int)get_input_format(AV_PIX_FMT_NONE), -1); +} + /* ========================================================================= * get_st_fps * ========================================================================= */ @@ -402,6 +442,103 @@ static void test_mtl_copy_yuv444p10le_full_frame(void **state) free_dst_frame(dst, bufs); } +/* ========================================================================= + * Session lifecycle / send — guard and error paths (no MTL hardware) + * ========================================================================= */ + +/* mtl_tx_session_free() with a NULL handle must be a safe no-op. */ +static void test_mtl_tx_session_free_null_handle(void **state) +{ + (void)state; + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.handle = NULL; + mtl_tx_session_free(&ctx); /* must not crash */ + assert_null(ctx.handle); +} + +/* mtl_tx_send_yuv_frame() returns -1 when the session handle is NULL. */ +static void test_mtl_tx_send_yuv_frame_null_handle(void **state) +{ + (void)state; + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + AVFrame *src = make_src_frame(AV_PIX_FMT_YUV422P10LE, 16, 16, 0xAA); + assert_non_null(src); + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, src, 0, 0, 16, 16), -1); + av_frame_free(&src); +} + +/* mtl_tx_send_yuv_frame() returns -1 when the src frame is NULL. */ +static void test_mtl_tx_send_yuv_frame_null_src(void **state) +{ + (void)state; + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.handle = (st20p_tx_handle)0x1; /* non-NULL; guard trips on src == NULL first */ + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, NULL, 0, 0, 16, 16), -1); +} + +/* mtl_tx_send_raw_yuv() returns -1 for each guard combination. */ +static void test_mtl_tx_send_raw_yuv_guards(void **state) +{ + (void)state; + struct st20p_tx_ctx ctx; + + /* NULL handle */ + memset(&ctx, 0, sizeof(ctx)); + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); + + /* handle set but no source buffer */ + memset(&ctx, 0, sizeof(ctx)); + ctx.handle = (st20p_tx_handle)0x1; + ctx.source_buffer = NULL; + ctx.source_size = 100; + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); + + /* handle + buffer set but zero source size */ + memset(&ctx, 0, sizeof(ctx)); + ctx.handle = (st20p_tx_handle)0x1; + uint8_t buf[16] = {0}; + ctx.source_buffer = buf; + ctx.source_size = 0; + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); +} + +/* mtl_tx_session_create() returns -1 (before touching MTL) when the pixel + * format is unsupported, exercising the ops-setup and error-return path. */ +static void test_mtl_tx_session_create_unsupported_fmt(void **state) +{ + (void)state; + struct dvledtx_context app; + memset(&app, 0, sizeof(app)); + assert_int_equal(dvledtx_context_alloc(&app, 1, 1), 0); + strncpy(app.nics[0].port, "0000:06:00.0", sizeof(app.nics[0].port) - 1); + app.fps = 30; + app.fmt = AV_PIX_FMT_NONE; /* unsupported → get_*_format() == -1 */ + app.udp_port = 20000; + app.payload_type = 96; + app.session_net[0].udp_port = 0; /* exercise app-default fallback */ + app.session_net[0].payload_type = 0; + app.session_net[0].nic_index = 0; + + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.idx = 0; + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + strncpy(ctx.session_name, "test_sess", sizeof(ctx.session_name) - 1); + + assert_int_equal(mtl_tx_session_create(&mgr, &ctx, &app, 0), -1); + assert_null(ctx.handle); + + dvledtx_context_free(&app); +} + /* ========================================================================= * main * ========================================================================= */ @@ -426,6 +563,13 @@ int main(void) cmocka_unit_test(test_get_input_format_yuv444p12le), cmocka_unit_test(test_get_input_format_gbrp12le), + /* get_input_format — 10-bit / 8-bit + unknown */ + cmocka_unit_test(test_get_input_format_yuv422p10le), + cmocka_unit_test(test_get_input_format_yuv420p), + cmocka_unit_test(test_get_input_format_yuv444p10le), + cmocka_unit_test(test_get_input_format_gbrp10le), + cmocka_unit_test(test_get_input_format_unknown_returns_error), + /* get_st_fps */ cmocka_unit_test(test_get_st_fps_25), cmocka_unit_test(test_get_st_fps_30), @@ -443,6 +587,13 @@ int main(void) cmocka_unit_test(test_mtl_copy_yuv422p10le_crop_offset), cmocka_unit_test(test_mtl_copy_yuv420p_full_frame), cmocka_unit_test(test_mtl_copy_yuv444p10le_full_frame), + + /* session lifecycle / send — guard and error paths */ + cmocka_unit_test(test_mtl_tx_session_free_null_handle), + cmocka_unit_test(test_mtl_tx_send_yuv_frame_null_handle), + cmocka_unit_test(test_mtl_tx_send_yuv_frame_null_src), + cmocka_unit_test(test_mtl_tx_send_raw_yuv_guards), + cmocka_unit_test(test_mtl_tx_session_create_unsupported_fmt), }; return cmocka_run_group_tests(tests, NULL, NULL); From 2fe7fec14a0c9e960066753e18e524bd3bb0575e Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 21 Jul 2026 13:11:15 +0530 Subject: [PATCH 3/6] test: add validation, resolve_ip, mtl copy-crop and tx crop-fail coverage Add config_reader validation branch tests (nic_index range, crop bounds, payload_type range, chroma alignment), resolve_ip_addrs tests, mtl_tx copy-crop error-path tests, and an ffmpeg_tx crop-failure test. Raises overall line coverage from 79% to 81%. --- tests/test_config_reader.c | 115 ++++++++++++++++++++++++++++++++++++ tests/test_ffmpeg_tx_mock.c | 34 +++++++++++ tests/test_mtl_tx.c | 30 ++++++++++ 3 files changed, 179 insertions(+) diff --git a/tests/test_config_reader.c b/tests/test_config_reader.c index 9800f67..c57053a 100644 --- a/tests/test_config_reader.c +++ b/tests/test_config_reader.c @@ -905,6 +905,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; @@ -1608,6 +1714,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), diff --git a/tests/test_ffmpeg_tx_mock.c b/tests/test_ffmpeg_tx_mock.c index 7537efc..36f5bae 100644 --- a/tests/test_ffmpeg_tx_mock.c +++ b/tests/test_ffmpeg_tx_mock.c @@ -382,6 +382,39 @@ static void test_ffmpeg_tx_send_yuv_frame_pipeline(void **state) close_ffmpeg_tx(&ctx); } +/* ffmpeg_tx_send_yuv_frame — crop rectangle exceeds source → crop_yuv_frame + * fails and the send returns -1 (covers the crop-failure error branch). */ +static void test_ffmpeg_tx_send_yuv_frame_bad_crop_fails(void **state) +{ + (void)state; + avdevice_register_all(); + + struct dvledtx_context app; + fill_app_16x16(&app, 1); + app.exit = false; + g_test_exit = false; + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.idx = 0; + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + + assert_int_equal(open_ffmpeg_tx(&ctx), 0); + assert_int_equal(load_video_source(&ctx, TEST_VIDEO_PATH), 0); + + assert_true(ffmpeg_decode_next_frame(&ctx)); + assert_non_null(ctx.yuv_frame); + + /* crop_x + crop_w = 8 + 16 = 24 > source width 16 → crop_yuv_frame fails */ + int ret = ffmpeg_tx_send_yuv_frame(&ctx, ctx.yuv_frame, 8, 0, 16, 16); + assert_int_equal(ret, -1); + + close_ffmpeg_source(&ctx); + close_ffmpeg_tx(&ctx); +} + /* ========================================================================= * open_ffmpeg_tx — multi-NIC redundant-port registration (idx 0, nic_count 2) * ========================================================================= */ @@ -505,6 +538,7 @@ int main(void) /* pipeline: decode + tx_send_yuv_frame */ cmocka_unit_test(test_ffmpeg_tx_send_yuv_frame_pipeline), + cmocka_unit_test(test_ffmpeg_tx_send_yuv_frame_bad_crop_fails), /* raw YUV send path */ cmocka_unit_test(test_ffmpeg_tx_send_raw_yuv_guard), diff --git a/tests/test_mtl_tx.c b/tests/test_mtl_tx.c index b6408b1..de8ffa3 100644 --- a/tests/test_mtl_tx.c +++ b/tests/test_mtl_tx.c @@ -289,6 +289,34 @@ static void test_mtl_copy_null_planes_no_crash(void **state) av_frame_free(&src); } +static void test_mtl_copy_invalid_crop_rect_no_copy(void **state) +{ + (void)state; + uint8_t *bufs[3] = {0}; + struct st_frame *dst = make_dst_frame(AV_PIX_FMT_YUV422P10LE, 16, 16, bufs); + assert_non_null(dst); + AVFrame *src = make_src_frame(AV_PIX_FMT_YUV422P10LE, 16, 16, 0xCC); + assert_non_null(src); + /* crop_w <= 0 → invalid rect branch returns early without copying */ + mtl_copy_crop_to_frame(dst, src, 0, 0, 0, 16, AV_PIX_FMT_YUV422P10LE); + av_frame_free(&src); + free_dst_frame(dst, bufs); +} + +static void test_mtl_copy_crop_exceeds_source_no_copy(void **state) +{ + (void)state; + uint8_t *bufs[3] = {0}; + struct st_frame *dst = make_dst_frame(AV_PIX_FMT_YUV422P10LE, 16, 16, bufs); + assert_non_null(dst); + AVFrame *src = make_src_frame(AV_PIX_FMT_YUV422P10LE, 16, 16, 0xDD); + assert_non_null(src); + /* crop_x + crop_w = 8 + 16 = 24 > src width 16 → exceeds-source branch */ + mtl_copy_crop_to_frame(dst, src, 8, 0, 16, 16, AV_PIX_FMT_YUV422P10LE); + av_frame_free(&src); + free_dst_frame(dst, bufs); +} + /* ========================================================================= * mtl_copy_crop_to_frame — data correctness * ========================================================================= */ @@ -581,6 +609,8 @@ int main(void) cmocka_unit_test(test_mtl_copy_null_dst_no_crash), cmocka_unit_test(test_mtl_copy_null_src_no_crash), cmocka_unit_test(test_mtl_copy_null_planes_no_crash), + cmocka_unit_test(test_mtl_copy_invalid_crop_rect_no_copy), + cmocka_unit_test(test_mtl_copy_crop_exceeds_source_no_copy), /* mtl_copy_crop_to_frame — data correctness */ cmocka_unit_test(test_mtl_copy_yuv422p10le_full_frame), From 78bf2a80b1a51ee2b7447cfc031ed89733d055d6 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 21 Jul 2026 14:24:32 +0530 Subject: [PATCH 4/6] test: address PR review comments on mtl copy-crop and display detection - Assert destination planes remain zero-filled after mtl_copy_crop_to_frame early-returns on invalid/exceeding crop rects, so the no_copy tests validate the contract rather than just absence of a crash. - Only append screen '.0' to the detected DISPLAY when it does not already specify a screen, avoiding invalid x11grab sources like ':0.0.0+0,0'. --- scripts/test.sh | 9 ++++++++- tests/test_mtl_tx.c | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/scripts/test.sh b/scripts/test.sh index 6c2eb99..9ccfc11 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -53,7 +53,14 @@ 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 - export DVLED_TEST_DISPLAY="${disp}.0+0,0" + # 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 diff --git a/tests/test_mtl_tx.c b/tests/test_mtl_tx.c index de8ffa3..8d345d4 100644 --- a/tests/test_mtl_tx.c +++ b/tests/test_mtl_tx.c @@ -254,6 +254,27 @@ static void free_dst_frame(struct st_frame *f, uint8_t **bufs) free(f); } +/* Verify every byte of the calloc'd destination planes is still zero, i.e. + * mtl_copy_crop_to_frame() wrote nothing (used by the invalid-rect tests to + * assert the early-return contract, not merely the absence of a crash). */ +static bool dst_planes_all_zero(uint8_t **bufs, enum AVPixelFormat fmt, + int crop_w, int crop_h) +{ + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(fmt); + int bps = (desc->comp[0].depth + 7) / 8; + int chroma_w = crop_w >> desc->log2_chroma_w; + int chroma_h = crop_h >> desc->log2_chroma_h; + size_t sizes[3] = { + (size_t)crop_w * crop_h * bps, + (size_t)chroma_w * chroma_h * bps, + (size_t)chroma_w * chroma_h * bps, + }; + for (int p = 0; p < 3; p++) + for (size_t i = 0; i < sizes[p]; i++) + if (bufs[p][i] != 0) return false; + return true; +} + /* ========================================================================= * mtl_copy_crop_to_frame — null guards * ========================================================================= */ @@ -299,6 +320,8 @@ static void test_mtl_copy_invalid_crop_rect_no_copy(void **state) assert_non_null(src); /* crop_w <= 0 → invalid rect branch returns early without copying */ mtl_copy_crop_to_frame(dst, src, 0, 0, 0, 16, AV_PIX_FMT_YUV422P10LE); + /* Contract: nothing was written — dst planes stay zero-filled. */ + assert_true(dst_planes_all_zero(bufs, AV_PIX_FMT_YUV422P10LE, 16, 16)); av_frame_free(&src); free_dst_frame(dst, bufs); } @@ -313,6 +336,8 @@ static void test_mtl_copy_crop_exceeds_source_no_copy(void **state) assert_non_null(src); /* crop_x + crop_w = 8 + 16 = 24 > src width 16 → exceeds-source branch */ mtl_copy_crop_to_frame(dst, src, 8, 0, 16, 16, AV_PIX_FMT_YUV422P10LE); + /* Contract: nothing was written — dst planes stay zero-filled. */ + assert_true(dst_planes_all_zero(bufs, AV_PIX_FMT_YUV422P10LE, 16, 16)); av_frame_free(&src); free_dst_frame(dst, bufs); } From d2717e831382fd144c998bde603e00dca125d277 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Tue, 21 Jul 2026 14:48:38 +0530 Subject: [PATCH 5/6] ci: drop unrelated CodeQL changes from coverage PR Remove the CodeQL workflow, CodeQL analysis action, and the CodeQL scan step in scan_on_demand.yml that were inadvertently included; this PR is scoped to test coverage and screen-capture only. --- .github/actions/analysis/codeql/action.yml | 27 ----------- .github/workflows/codeql.yml | 53 ---------------------- .github/workflows/scan_on_demand.yml | 3 -- 3 files changed, 83 deletions(-) delete mode 100644 .github/actions/analysis/codeql/action.yml delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/actions/analysis/codeql/action.yml b/.github/actions/analysis/codeql/action.yml deleted file mode 100644 index 1296205..0000000 --- a/.github/actions/analysis/codeql/action.yml +++ /dev/null @@ -1,27 +0,0 @@ -# -# BSD 3-Clause License -# Copyright (C) 2026 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# -name: 'CodeQL Scan' -description: 'Initialize CodeQL, build dvledtx, and perform CodeQL analysis for C/C++' - -runs: - using: composite - steps: - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - with: - languages: c-cpp - build-mode: manual - - # `c-cpp` always requires a manual build so CodeQL can trace the - # compiler invocations produced by meson/ninja. - - name: Build dvledtx - uses: ./.github/actions/build-dvledtx - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - with: - category: "/language:c-cpp" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 58d9afd..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,53 +0,0 @@ -# -# BSD 3-Clause License -# Copyright (C) 2026 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# -# CodeQL Advanced workflow for dvledtx (C/C++ project built with meson/ninja). -# -# This repository only contains C sources, so the language matrix has been -# reduced to `c-cpp` and the build mode set to `manual`, reusing the existing -# environment-check/build-dvledtx composite actions so CodeQL traces the same -# build the rest of CI performs. -name: CodeQL Scan - -on: - schedule: - # Weekly on Monday at 07:19 UTC - - cron: '19 7 * * 1' - workflow_dispatch: - -permissions: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - codeql: - name: CodeQL Scan - runs-on: ubuntu-latest - timeout-minutes: 45 - permissions: - contents: read - security-events: write # Required to upload SARIF results to GitHub Security tab - - steps: - - name: Clean up previous run - env: - WORKSPACE: ${{ github.workspace }} - run: | - find "$WORKSPACE" -mindepth 1 -maxdepth 1 -exec rm -rf {} + - - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - fetch-depth: 0 - persist-credentials: false - - - name: Environment check - uses: ./.github/actions/environment-check - - - name: CodeQL Scan - uses: ./.github/actions/analysis/codeql diff --git a/.github/workflows/scan_on_demand.yml b/.github/workflows/scan_on_demand.yml index 7e523ee..3214385 100644 --- a/.github/workflows/scan_on_demand.yml +++ b/.github/workflows/scan_on_demand.yml @@ -114,9 +114,6 @@ jobs: - name: Trivy Scan uses: ./.github/actions/analysis/trivy - - name: CodeQL Scan - uses: ./.github/actions/analysis/codeql - - name: Coverity Scan uses: ./.github/actions/analysis/coverity env: From b327acad96b73a7269efcecc003cee013b87c8c2 Mon Sep 17 00:00:00 2001 From: roshan-ku Date: Wed, 22 Jul 2026 12:02:47 +0530 Subject: [PATCH 6/6] test: cover MTL TX paths with link-time stubs (no hardware) Add test_mtl_tx_stub.c wrapping the MTL library calls (mtl_init/uninit, st20p_tx_create/free/get_frame/put_frame, etc.) to exercise mtl_tx_init, mtl_tx_session_create/free and the send paths without a NIC/DPDK. Raises src/mtl/mtl_tx.c coverage from 55% to 98%. --- tests/meson.build | 23 ++ tests/test_mtl_tx_stub.c | 548 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 571 insertions(+) create mode 100644 tests/test_mtl_tx_stub.c diff --git a/tests/meson.build b/tests/meson.build index 90cb745..aa6aa60 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -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 diff --git a/tests/test_mtl_tx_stub.c b/tests/test_mtl_tx_stub.c new file mode 100644 index 0000000..21d91f0 --- /dev/null +++ b/tests/test_mtl_tx_stub.c @@ -0,0 +1,548 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright 2026 Intel Corporation + * + * Stub-based unit tests for the hardware-facing functions in src/mtl/mtl_tx.c. + * + * Strategy + * -------- + * The MTL library entry points are intercepted at link time with + * `-Wl,--wrap=` so NO real MTL/DPDK hardware is touched. Each wrapper + * returns a caller-controlled value via a small set of file-scope globals, + * letting us exercise every success and failure branch of: + * + * mtl_tx_init() — MTL library init (success / mtl_init NULL) + * mtl_tx_uninit() — release (handle present / already NULL) + * mtl_tx_session_create() — ST20P session (success / bad fmt / create NULL / + * udp_port + payload_type defaulting) + * mtl_tx_session_free() — free (handle present / already NULL) + * mtl_tx_send_yuv_frame() — get/copy/put (guards / get NULL / put fail / + * per-session + shared-decoder timestamp paths) + * mtl_tx_send_raw_yuv() — raw buffer send (guards / get NULL / put fail / + * loop wrap-around) + * + * Compiled with -DENABLE_MTL_TX so the whole file is in scope. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "app_context.h" +#include "core/session_manager.h" +#include "mtl/mtl_tx.h" +#include "util/logger.h" + +/* ========================================================================= + * Wrapper control state + MTL library stubs (--wrap targets) + * ========================================================================= */ +static char g_mtl_obj; /* dummy backing object for a fake mtl_handle */ +static char g_sess_obj; /* dummy backing object for a fake st20p handle */ +#define FAKE_MTL ((mtl_handle)&g_mtl_obj) +#define FAKE_SESS ((st20p_tx_handle)&g_sess_obj) + +static mtl_handle g_init_ret; /* value __wrap_mtl_init returns */ +static st20p_tx_handle g_create_ret; /* value __wrap_st20p_tx_create returns */ +static size_t g_frame_size_ret; /* value __wrap_st20p_tx_frame_size returns*/ +static struct st_frame* g_get_frame_ret; /* value __wrap_st20p_tx_get_frame returns */ +static int g_put_frame_ret; /* value __wrap_st20p_tx_put_frame returns */ +static int g_uninit_calls; /* how many times mtl_uninit was invoked */ +static int g_free_calls; /* how many times st20p_tx_free was invoked*/ + +mtl_handle __wrap_mtl_init(struct mtl_init_params* p); +int __wrap_mtl_uninit(mtl_handle mt); +enum mtl_pmd_type __wrap_mtl_pmd_by_port_name(const char* port); +st20p_tx_handle __wrap_st20p_tx_create(mtl_handle mt, struct st20p_tx_ops* ops); +int __wrap_st20p_tx_free(st20p_tx_handle handle); +size_t __wrap_st20p_tx_frame_size(st20p_tx_handle handle); +struct st_frame* __wrap_st20p_tx_get_frame(st20p_tx_handle handle); +int __wrap_st20p_tx_put_frame(st20p_tx_handle handle, struct st_frame* frame); + +mtl_handle __wrap_mtl_init(struct mtl_init_params* p) { (void)p; return g_init_ret; } +int __wrap_mtl_uninit(mtl_handle mt) { (void)mt; g_uninit_calls++; return 0; } +enum mtl_pmd_type __wrap_mtl_pmd_by_port_name(const char* port) { + (void)port; return MTL_PMD_DPDK_USER; +} +st20p_tx_handle __wrap_st20p_tx_create(mtl_handle mt, struct st20p_tx_ops* ops) { + (void)mt; (void)ops; return g_create_ret; +} +int __wrap_st20p_tx_free(st20p_tx_handle handle) { (void)handle; g_free_calls++; return 0; } +size_t __wrap_st20p_tx_frame_size(st20p_tx_handle handle) { (void)handle; return g_frame_size_ret; } +struct st_frame* __wrap_st20p_tx_get_frame(st20p_tx_handle handle) { (void)handle; return g_get_frame_ret; } +int __wrap_st20p_tx_put_frame(st20p_tx_handle handle, struct st_frame* frame) { + (void)handle; (void)frame; return g_put_frame_ret; +} + +/* ========================================================================= + * Fixtures + * ========================================================================= */ +static int setup(void **state) +{ + (void)state; + g_init_ret = FAKE_MTL; + g_create_ret = FAKE_SESS; + g_frame_size_ret = 512; + g_get_frame_ret = NULL; + g_put_frame_ret = 0; + g_uninit_calls = 0; + g_free_calls = 0; + return 0; +} + +/* Build a minimal app context with `nics` NICs and `sessions` sessions. */ +static void app_init(struct dvledtx_context* app, int nics, int sessions) +{ + memset(app, 0, sizeof(*app)); + app->nic_count = nics; + app->nics = calloc((size_t)nics, sizeof(struct nic_config)); + assert_non_null(app->nics); + for (int i = 0; i < nics; i++) + snprintf(app->nics[i].port, PORT_NAME_LEN, "0000:00:%02d.0", i); + + app->st20p_sessions = sessions; + app->session_net = calloc((size_t)sessions, sizeof(struct tx_session_net)); + assert_non_null(app->session_net); + for (int i = 0; i < sessions; i++) { + app->session_net[i].nic_index = 0; + app->session_net[i].udp_port = (uint16_t)(20000 + i * 2); + app->session_net[i].payload_type = 96; + } + app->fps = 30; + app->fmt = AV_PIX_FMT_YUV422P10LE; + app->udp_port = 20000; + app->payload_type = 96; +} + +static void app_free(struct dvledtx_context* app) +{ + free(app->nics); + free(app->session_net); +} + +/* Allocate an st_frame with tightly-packed YUV422P10LE 16x16 plane buffers. */ +static struct st_frame* make_st_frame(void) +{ + struct st_frame* f = calloc(1, sizeof(*f)); + assert_non_null(f); + f->addr[0] = calloc(1, 16 * 16 * 2); /* Y = 512 */ + f->addr[1] = calloc(1, 8 * 16 * 2); /* Cb = 256 */ + f->addr[2] = calloc(1, 8 * 16 * 2); /* Cr = 256 */ + assert_non_null(f->addr[0]); + return f; +} + +static void free_st_frame(struct st_frame* f) +{ + free(f->addr[0]); + free(f->addr[1]); + free(f->addr[2]); + free(f); +} + +/* Allocate a decoded 16x16 YUV422P10LE source AVFrame with real plane data. */ +static AVFrame* make_src_frame(void) +{ + AVFrame* src = av_frame_alloc(); + assert_non_null(src); + src->width = 16; + src->height = 16; + src->format = AV_PIX_FMT_YUV422P10LE; + assert_int_equal(av_frame_get_buffer(src, 0), 0); + return src; +} + +/* ========================================================================= + * mtl_tx_init / mtl_tx_uninit + * ========================================================================= */ +static void test_mtl_tx_init_success(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 2, 3); + /* One session points at an out-of-range NIC to exercise the guard. */ + app.session_net[2].nic_index = 99; + + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + g_init_ret = FAKE_MTL; + + assert_int_equal(mtl_tx_init(&mgr, &app), 0); + assert_ptr_equal(mgr.mtl, FAKE_MTL); + + app_free(&app); +} + +static void test_mtl_tx_init_failure(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + g_init_ret = NULL; /* mtl_init fails */ + + assert_int_equal(mtl_tx_init(&mgr, &app), -1); + assert_null(mgr.mtl); + + app_free(&app); +} + +static void test_mtl_tx_uninit(void **state) +{ + (void)state; + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + + mgr.mtl = FAKE_MTL; + mtl_tx_uninit(&mgr); + assert_null(mgr.mtl); + assert_int_equal(g_uninit_calls, 1); + + /* Second call is a no-op (handle already NULL). */ + mtl_tx_uninit(&mgr); + assert_int_equal(g_uninit_calls, 1); +} + +/* ========================================================================= + * mtl_tx_session_create / mtl_tx_session_free + * ========================================================================= */ +static void test_session_create_success(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + mgr.mtl = FAKE_MTL; + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + snprintf(ctx.session_name, sizeof(ctx.session_name), "s0"); + + g_create_ret = FAKE_SESS; + g_frame_size_ret = 512; + + assert_int_equal(mtl_tx_session_create(&mgr, &ctx, &app, 0), 0); + assert_ptr_equal(ctx.handle, FAKE_SESS); + assert_int_equal((int)ctx.frame_size, 512); + + app_free(&app); +} + +static void test_session_create_defaults_port_and_pt(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + /* Force the udp_port==0 and payload_type==0 defaulting branches. */ + app.session_net[0].udp_port = 0; + app.session_net[0].payload_type = 0; + + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + mgr.mtl = FAKE_MTL; + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + + g_create_ret = FAKE_SESS; + assert_int_equal(mtl_tx_session_create(&mgr, &ctx, &app, 0), 0); + + app_free(&app); +} + +static void test_session_create_bad_format(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + app.fmt = AV_PIX_FMT_RGB24; /* unsupported -> transport/input fmt == -1 */ + + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + mgr.mtl = FAKE_MTL; + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + + assert_int_equal(mtl_tx_session_create(&mgr, &ctx, &app, 0), -1); + assert_null(ctx.handle); + + app_free(&app); +} + +static void test_session_create_handle_null(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + session_manager_t mgr; + memset(&mgr, 0, sizeof(mgr)); + mgr.mtl = FAKE_MTL; + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.crop_width = 16; + ctx.crop_height = 16; + + g_create_ret = NULL; /* st20p_tx_create fails */ + assert_int_equal(mtl_tx_session_create(&mgr, &ctx, &app, 0), -1); + assert_null(ctx.handle); + + app_free(&app); +} + +static void test_session_free(void **state) +{ + (void)state; + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + + ctx.handle = FAKE_SESS; + mtl_tx_session_free(&ctx); + assert_null(ctx.handle); + assert_int_equal(g_free_calls, 1); + + /* Already NULL -> no-op */ + mtl_tx_session_free(&ctx); + assert_int_equal(g_free_calls, 1); +} + +/* ========================================================================= + * mtl_tx_send_yuv_frame + * ========================================================================= */ +static void test_send_yuv_guards(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + + /* NULL handle */ + ctx.handle = NULL; + AVFrame* src = make_src_frame(); + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, src, 0, 0, 16, 16), -1); + + /* NULL src */ + ctx.handle = FAKE_SESS; + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, NULL, 0, 0, 16, 16), -1); + + /* get_frame returns NULL */ + g_get_frame_ret = NULL; + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, src, 0, 0, 16, 16), -1); + + av_frame_free(&src); + app_free(&app); +} + +static void test_send_yuv_success_per_session(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.handle = FAKE_SESS; + ctx.frames_sent = 99; /* next send hits the "% 100 == 0" log branch */ + + AVFrame* src = make_src_frame(); + struct st_frame* frame = make_st_frame(); + g_get_frame_ret = frame; + g_put_frame_ret = 0; + + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, src, 0, 0, 16, 16), 0); + assert_int_equal((int)ctx.frames_sent, 100); + + free_st_frame(frame); + av_frame_free(&src); + app_free(&app); +} + +static void test_send_yuv_success_shared_decoder(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + struct shared_decode_ctx sd; + memset(&sd, 0, sizeof(sd)); + sd.frame_counter = 7; + + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.handle = FAKE_SESS; + ctx.shared_dec = &sd; /* timestamp derived from shared frame_counter */ + + AVFrame* src = make_src_frame(); + struct st_frame* frame = make_st_frame(); + g_get_frame_ret = frame; + g_put_frame_ret = 0; + + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, src, 0, 0, 16, 16), 0); + assert_int_equal((int)ctx.frames_sent, 1); + + free_st_frame(frame); + av_frame_free(&src); + app_free(&app); +} + +static void test_send_yuv_put_fail(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.handle = FAKE_SESS; + + AVFrame* src = make_src_frame(); + struct st_frame* frame = make_st_frame(); + g_get_frame_ret = frame; + g_put_frame_ret = -1; /* put_frame fails */ + + assert_int_equal(mtl_tx_send_yuv_frame(&ctx, src, 0, 0, 16, 16), -1); + + free_st_frame(frame); + av_frame_free(&src); + app_free(&app); +} + +/* ========================================================================= + * mtl_tx_send_raw_yuv + * ========================================================================= */ +static void test_send_raw_guards(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + + /* NULL handle */ + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); + + /* handle set but no source buffer */ + ctx.handle = FAKE_SESS; + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); + + /* source buffer set but source_size == 0 */ + uint8_t buf[2048]; + ctx.source_buffer = buf; + ctx.source_size = 0; + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); + + /* get_frame returns NULL */ + ctx.source_size = sizeof(buf); + ctx.frame_size = 512; + g_get_frame_ret = NULL; + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); + + app_free(&app); +} + +static void test_send_raw_success_and_wraparound(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.handle = FAKE_SESS; + + uint8_t buf[2048]; + memset(buf, 0xAB, sizeof(buf)); + ctx.source_buffer = buf; + ctx.source_size = sizeof(buf); + ctx.frame_size = 512; + + struct st_frame* frame = make_st_frame(); + g_get_frame_ret = frame; + g_put_frame_ret = 0; + + /* Normal send: current_pos advances by frame_size. */ + ctx.current_pos = 0; + ctx.frames_sent = 99; /* hit the "% 100 == 0" log branch */ + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), 0); + assert_int_equal((int)ctx.current_pos, 512); + assert_int_equal((int)ctx.frames_sent, 100); + + /* Wrap-around: current_pos + frame_size > source_size resets to 0. */ + ctx.current_pos = 1800; /* 1800 + 512 = 2312 > 2048 */ + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), 0); + assert_int_equal((int)ctx.current_pos, 512); + + free_st_frame(frame); + app_free(&app); +} + +static void test_send_raw_put_fail(void **state) +{ + (void)state; + struct dvledtx_context app; + app_init(&app, 1, 1); + struct st20p_tx_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.app = &app; + ctx.handle = FAKE_SESS; + + uint8_t buf[2048]; + ctx.source_buffer = buf; + ctx.source_size = sizeof(buf); + ctx.frame_size = 512; + + struct st_frame* frame = make_st_frame(); + g_get_frame_ret = frame; + g_put_frame_ret = -1; /* put_frame fails */ + + assert_int_equal(mtl_tx_send_raw_yuv(&ctx), -1); + + free_st_frame(frame); + app_free(&app); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup(test_mtl_tx_init_success, setup), + cmocka_unit_test_setup(test_mtl_tx_init_failure, setup), + cmocka_unit_test_setup(test_mtl_tx_uninit, setup), + cmocka_unit_test_setup(test_session_create_success, setup), + cmocka_unit_test_setup(test_session_create_defaults_port_and_pt, setup), + cmocka_unit_test_setup(test_session_create_bad_format, setup), + cmocka_unit_test_setup(test_session_create_handle_null, setup), + cmocka_unit_test_setup(test_session_free, setup), + cmocka_unit_test_setup(test_send_yuv_guards, setup), + cmocka_unit_test_setup(test_send_yuv_success_per_session, setup), + cmocka_unit_test_setup(test_send_yuv_success_shared_decoder, setup), + cmocka_unit_test_setup(test_send_yuv_put_fail, setup), + cmocka_unit_test_setup(test_send_raw_guards, setup), + cmocka_unit_test_setup(test_send_raw_success_and_wraparound, setup), + cmocka_unit_test_setup(test_send_raw_put_fail, setup), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}