iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0

我們寫好的插件若是要可靠,當然得替他寫出可重複利用的測試,GStreamer有提供一套測試框架叫gstcheck,它提供一套完整的機制來對元件進行單元測試,而我們今天的目標就是用這個測試框架來驗證ironmancolormagic是否可以將輸入的GstBuffer處理成正確的像素

建立測試專案

先建立測試專案的資料夾

mkdir -p /ironman/source/gst/colormagic/test

然後在資料夾底下建立test_ironmancolormagic.c

#include <gst/video/video.h>
#include <gst/check/gstcheck.h>
#include <gst/check/gstharness.h>
#include <cuda_runtime_api.h>
#include <nvbufsurface.h>
#include <gstnvdsmeta.h>

#define TEST_GPU_ID (0)

typedef struct {
  guint8 r;
  guint8 g;
  guint8 b;
  guint8 a;
} Color;

static NvBufSurface*
create_test_surface(guint batch_size, guint width, guint height)
{
    NvBufSurface* surf;
    NvBufSurfaceCreateParams params;

    params.gpuId = TEST_GPU_ID;
    params.width = width;
    params.height = height;
    params.size = 0;
    /* Our element only accepts RGBA input */
    params.colorFormat = NVBUF_COLOR_FORMAT_RGBA;
    params.memType = NVBUF_MEM_DEFAULT;

    if (NvBufSurfaceCreate(&surf, batch_size, &params) < 0) {
        g_error("Failed create NvBufSurface");
    }

    return surf;
}

static void
destroy_surface(gpointer data)
{
    if (data)
    {
        NvBufSurface* surf = data;
        NvBufSurfaceDestroy(surf);
    }
}

static void
copy_image_to_surface(const NvBufSurface* surf, guint batch_id, guint8* data)
{
    g_return_if_fail(surf != NULL);
    g_return_if_fail(batch_id < surf->batchSize);
    g_return_if_fail(data != NULL);

    cudaError_t cuda_err;

    cuda_err = cudaSetDevice((gint)surf->gpuId);
    if (cuda_err != cudaSuccess)
    {
        g_critical("cudaSetDevice failed: %s", cudaGetErrorString(cuda_err));
        return;
    }

    const NvBufSurfaceParams* surf_params = &surf->surfaceList[batch_id];
    g_return_if_fail(surf_params->colorFormat == NVBUF_COLOR_FORMAT_RGBA);
    const gsize data_pitch = (gsize)surf_params->width * 4;
    const gsize data_width_bytes = (gsize)surf_params->width * 4;

    cuda_err = cudaMemcpy2D(surf_params->dataPtr,
        surf_params->pitch,
        data,
        data_pitch,
        data_width_bytes,
        surf_params->height,
        cudaMemcpyHostToDevice);
    if (cuda_err != cudaSuccess)
    {
        g_critical("cudaMemcpy2D failed: %s", cudaGetErrorString(cuda_err));
        return;
    }
}

static guint8*
copy_image_from_surface(const NvBufSurface* surf, guint batch_id)
{
    g_return_val_if_fail(surf != NULL, NULL);
    g_return_val_if_fail(batch_id < surf->batchSize, NULL);

    cudaError_t cuda_err;

    cuda_err = cudaSetDevice((gint)surf->gpuId);
    if (cuda_err != cudaSuccess)
    {
        g_critical("cudaSetDevice failed: %s", cudaGetErrorString(cuda_err));
        return NULL;
    }

    const NvBufSurfaceParams* surf_params = &surf->surfaceList[batch_id];
    g_return_val_if_fail(surf_params->colorFormat == NVBUF_COLOR_FORMAT_RGBA, NULL);

    const gsize data_pitch = (gsize)surf_params->width * 4;
    const gsize data_width_bytes = (gsize)surf_params->width * 4;
    const gsize data_size = data_pitch * surf_params->height;
    guint8* data = g_malloc0(data_size);

    cuda_err = cudaMemcpy2D(data,
        data_pitch,
        surf_params->dataPtr,
        surf_params->pitch,
        data_width_bytes,
        surf_params->height,
        cudaMemcpyDeviceToHost);
    if (cuda_err != cudaSuccess)
    {
        g_critical("cudaMemcpy2D failed: %s", cudaGetErrorString(cuda_err));
        g_free(data);
        return NULL;
    }

    return data;
}

static NvDsBatchMeta* create_batch_meta_from_surface(const NvBufSurface* surf)
{
    g_return_val_if_fail(surf != NULL, NULL);
    g_return_val_if_fail(surf->batchSize > 0, NULL);
    g_return_val_if_fail(surf->numFilled > 0 && surf->numFilled <= surf->batchSize, NULL);

    NvDsBatchMeta* batch_meta = nvds_create_batch_meta(surf->batchSize);
    if (!batch_meta)
    {
        g_error("Failed to create NvDsBatchMeta");
    }

    for (guint i = 0; i < surf->numFilled; i++)
    {
        NvDsFrameMeta* frame_meta = nvds_acquire_frame_meta_from_pool(batch_meta);

        /* We only care `batch_id` in `ironmancolormagic`, the other fields
         * don't affect the result and we don't need to fill them */
        frame_meta->batch_id = i;

        nvds_add_frame_meta_to_batch(batch_meta, frame_meta);
    }

    return batch_meta;
}

static gboolean
attach_batch_meta_to_buffer(GstBuffer* buf, NvDsBatchMeta* batch_meta)
{
    g_return_val_if_fail(buf != NULL, FALSE);

    NvDsMeta* meta = gst_buffer_add_nvds_meta(buf,
        batch_meta,
        NULL,
        nvds_batch_meta_copy_func,
        nvds_batch_meta_release_func);

    if (!meta)
    {
        g_critical("Failed to add NvDsBatchMeta to the GstBuffer");
        return FALSE;
    }

    /* Must set 'meta_type' manually */
    meta->meta_type = NVDS_BATCH_GST_META;

    return TRUE;
}

static void
set_uniform_color(guint width, guint height, guint8* data, Color color)
{
    g_return_if_fail(width > 0);
    g_return_if_fail(height > 0);
    g_return_if_fail(data != NULL);

    const gsize stride = width * 4;

    for (gsize y = 0; y < height; y++)
    {
        for (gsize x = 0; x < width; x++)
        {
            Color* pixel = (Color*)&data[stride * y + x * 4];
            *pixel = color;
        }
    }
}

static gboolean
is_blob_equals(const guint8* a, const guint8* b, gsize size)
{
    g_return_val_if_fail(a != NULL, FALSE);
    g_return_val_if_fail(b != NULL, FALSE);

    for (gsize i = 0; i < size; i++)
    {
        if (a[i] != b[i])
            return FALSE;
    }

    return TRUE;
}

#define FEATURE_NVMM "memory:NVMM"

static GstStaticPadTemplate upstream_src_pad =
GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES(FEATURE_NVMM, "{RGBA}"))
    );

static GstStaticPadTemplate downstream_sink_pad =
GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES(FEATURE_NVMM, "{RGBA}"))
    );

GST_START_TEST(test_transform_color)
{
    /* Set up element */
    GstElement* sut = gst_element_factory_make("ironmancolormagic", NULL);
    fail_unless(sut != NULL);
    g_object_set(sut, "gpu-id", TEST_GPU_ID, NULL);

    /* Create a harness and add the element to it, the harness will play after created */
    GstHarness* h = gst_harness_new_full(sut,
        &upstream_src_pad,
        "sink",
        &downstream_sink_pad,
        "src");
    fail_unless(h != NULL);

    {
        GstCaps* caps = gst_caps_from_string(
            "video/x-raw(memory:NVMM), format=RGBA, width=16, height=16");
        /* Must call this function before pushing a GstBuffer */
        gst_harness_set_src_caps(h, caps);
    }

    /* Prepare GstBuffer for testing */
    NvBufSurface* surf = create_test_surface(1, 16, 16);
    fail_unless(surf != NULL);

    GstBuffer* in_buf = gst_buffer_new_wrapped_full(
        GST_MEMORY_FLAG_READONLY,
        surf,
        sizeof(NvBufSurface),
        0,
        sizeof(NvBufSurface),
        surf,
        destroy_surface);

    const gsize image_data_size = (gsize)16 * 4 * 16;
    guint8* image_data = g_malloc0(image_data_size);
    set_uniform_color(16, 16, image_data, (Color){150, 100, 50, 255});
    copy_image_to_surface(surf, 0, image_data);
    surf->numFilled = 1;

    {
        NvDsBatchMeta* batch_meta = create_batch_meta_from_surface(surf);
        fail_unless(batch_meta != NULL);
        fail_unless(attach_batch_meta_to_buffer(in_buf, batch_meta) == TRUE);
    }

    /* We only support transform_ip, so the input buffer
     * and the output buffer should be the same instance */
    gst_harness_push(h, in_buf);
    GstBuffer* out_buf = gst_harness_pull(h);

    fail_unless(in_buf == out_buf);

    set_uniform_color(16, 16, image_data, (Color){100, 50, 150, 255});
    guint8* result_image_data = copy_image_from_surface(surf, 0);

    fail_unless(is_blob_equals(image_data, result_image_data, image_data_size) == TRUE);

    g_free(image_data);
    g_free(result_image_data);
    gst_buffer_unref(out_buf);
    gst_harness_teardown(h);
}
GST_END_TEST;

static Suite*
ironmancolormagic_suite(void)
{
    Suite* s = suite_create("ironmancolormagic_test");
    TCase* tc = tcase_create("change_color");

    suite_add_tcase(s, tc);

    tcase_add_test(tc, test_transform_color);

    return s;
}

GST_CHECK_MAIN(ironmancolormagic);

建立Makefile

TARGET := test_ironmancolormagic
CUDA_DIR := /usr/local/cuda
DS_DIR := /opt/nvidia/deepstream/deepstream

CC := gcc

PKGS := gstreamer-check-1.0

CFLAGS := -g -Wall -Wextra $(shell pkg-config --cflags $(PKGS)) \
	-I$(CUDA_DIR)/include \
	-I$(DS_DIR)/sources/includes

LDFLAGS := $(shell pkg-config --libs $(PKGS)) \
	-L$(CUDA_DIR)/lib64 -lcudart \
	-L$(DS_DIR)/lib -lnvbufsurface -lnvds_meta -lnvdsgst_meta \
	-Wl,-rpath,"$(CUDA_DIR)/lib64:$(DS_DIR)/lib"

SRCS := test_ironmancolormagic.c
OBJS := $(SRCS:.c=.o)

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

%.o: %.cu
	$(NVCC) $(NVCC_CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)

test: all
	GST_PLUGIN_PATH="../src" ./$(TARGET)

debug_test: all
	GST_PLUGIN_PATH="../src" CK_FORK=no gdb ./$(TARGET)

.PHONY: all clean test debug_test

接著就可以編譯測試看看

make test

應該會看到這樣的輸出,這樣就代表成功了

GST_PLUGIN_PATH="../src" ./test_ironmancolormagic
Running suite(s): ironmancolormagic_test
100%: Checks: 1, Failures: 0, Errors: 0
Check suite ironmancolormagic ran in 0.121s (tests failed: 0)

測試程式解說

如何用GstCheck建立測試程式

一個測試用例(Test Case)必須是以GST_START_TEST開頭,GST_END_TEST結尾,參考範例中的test_transform_color;再來要建立一個測試套件(Test Suite)去包裝你寫好的各種測試用例,參考範例中的ironmancolormagic_suite;最後一步就是定義GST_CHECK_MAIN,這樣執行檔就用GstCheck這個框架去執行測試

程式的運行模式

GstCheck預設會啟動子進程執行每一個測試,我們Makefile中的規則test就是用預設模式去執行。

test: all
	GST_PLUGIN_PATH="../src" ./$(TARGET)

但是當測試不是那麼順利時,我們必須得從測試程式開始往下除錯,這時會面臨沒有辦法讓Debugger中斷在子進程的問題,所以規則debug_test多加上了CK_FORK=no來停用產生子進程這個運行模式

debug_test: all
	GST_PLUGIN_PATH="../src" CK_FORK=no gdb ./$(TARGET)

至於這次的範例我是用gdb進行除錯,如果程式發生崩潰gdb可以直接查詢崩潰當下的Stack Trace,對於一般的除錯工作其實就很夠用了,如果你有興趣的話可以試著練習使用

建立測試元件

    /* Set up element */
    GstElement* sut = gst_element_factory_make("ironmancolormagic", NULL);
    fail_unless(sut != NULL);
    g_object_set(sut, "gpu-id", TEST_GPU_ID, NULL);

因為GstHarness建立之後會自動進入PLAYING狀態,所以我們得在建立GstHarness之前先將GstElement建立好並且設定屬性

建立GstHarness

    /* Create a harness and add the element to it, the harness will play after created */
    GstHarness* h = gst_harness_new_full(sut,
        &upstream_src_pad,
        "sink",
        &downstream_sink_pad,
        "src");
    fail_unless(h != NULL);

其實GstHarness可以視為GStreamer提供的Mocking Library,它提供了假的上游Src Pad和下游Sink Pad,開發者可以使用GstHarness完美的模擬元件的各種輸入並取得輸出

    {
        GstCaps* caps = gst_caps_from_string(
            "video/x-raw(memory:NVMM), format=RGBA, width=16, height=16");
        /* Must call this function before pushing a GstBuffer */
        gst_harness_set_src_caps(h, caps);
    }

接著就是輸入GstBuffer前要記得呼叫gst_harness_set_src_caps,這樣GStreamer才會把Caps固定到測試狀態,否則會處於未定狀態並發出警告

建立GstBuffer

    NvBufSurface* surf = create_test_surface(1, 16, 16);
    fail_unless(surf != NULL);

    GstBuffer* in_buf = gst_buffer_new_wrapped_full(
        GST_MEMORY_FLAG_READONLY,
        surf,
        sizeof(NvBufSurface),
        0,
        sizeof(NvBufSurface),
        surf,
        destroy_surface);

NvBufSurface得使用DeepStream SDK的函式分配/釋放資源,所以這邊得用到gst_buffer_new_wrapped_full建立GstBuffer。
GstBuffer本身只是資料的容器,這邊需要做的就是告訴它我的資料有多大,注意是資料本身有多大,也就是NvBufSurface本身有多大

使用gst_buffer_new_wrapped_full如果要釋放資源,必須傳入user_data並搭配notify,為什麼是這樣呢?因為你要封入GstBuffer的資料可能只是你分配資源的某一個片段,所以得透過這種間接的手段去釋放資源。想知道DeepStream元件如何建立GstBuffer的話,可以去參考原始碼/opt/nvidia/deepstream/deepstream/sources/gst-plugins/gst-nvmultistream2/gstnvstreammux_impl.h的148行

複製測試影像

    const gsize image_data_size = (gsize)16 * 4 * 16;
    guint8* image_data = g_malloc0(image_data_size);
    set_uniform_color(16, 16, image_data, (Color){150, 100, 50, 255});
    copy_image_to_surface(surf, 0, image_data);
    surf->numFilled = 1;

要驗證元件的行為是否正確的話,我們得傳入一個已知像素的影像,這樣才能用已知輸出去驗證,這邊就是輸入一個16x16的RGBA影像,每個像素都是(150, 100, 50, 255),元件要把像素從RGBA轉成GBRA,所以輸出應該要得到每個像素都是(100, 50, 150, 255)

這邊是寫測試所以不必特別考慮效能,我們可以先在Host設定好測試影像再複製到NvBufSurfaceParams上,你總不會為了測試特別寫個CUDA Kernel對吧,詳細的做法可以看set_uniform_color和copy_image_to_surface。這邊提一下cudaMemcpy2D,這是複製二維影像的一個高效API,它可以允許src和dst擁有不同大小的pitch,而這正好是我們需要的,我們可以讓image_data分配剛好的大小,而NvBufSurface分配帶有特定pitch的大小

掛上NvDsBatchMeta

    {
        NvDsBatchMeta* batch_meta = create_batch_meta_from_surface(surf);
        fail_unless(batch_meta != NULL);
        fail_unless(attach_batch_meta_to_buffer(in_buf, batch_meta) == TRUE);
    }

我們的元件需要NvDsBatchMeta來找到正確的NvBufSurfaceParams,所以測試流程也必須把NvDsBatchMeta掛到GstBuffer上。建立Meta的部分比較簡單,我們只需要用到NvDsFrameMeta::batch_id,所以把資料填對就行。新手比較要注意的是附加Meta用到的gst_buffer_add_nvds_meta,一定要記得設定回傳的NvDsMeta::meta_type,這邊要填入的是NVDS_BATCH_GST_META,如果你沒有做這一步,你的元件會撈不到NvDsBatchMeta

推入GstBuffer並驗證結果

    /* We only support transform_ip, so the input buffer
     * and the output buffer should be the same instance */
    gst_harness_push(h, in_buf);
    GstBuffer* out_buf = gst_harness_pull(h);

    fail_unless(in_buf == out_buf);

    set_uniform_color(16, 16, image_data, (Color){100, 50, 150, 255});
    guint8* result_image_data = copy_image_from_surface(surf, 0);

    fail_unless(is_blob_equals(image_data, result_image_data, image_data_size) == TRUE);

因為元件只支持transform_ip,輸入跟輸出的GstBuffer是同一個,所以這裡使用gst_harness_push和gst_harness_pull應該要得到同一個GstBuffer

再來就是要把影像從NvBufSurfaceParams::dataPtr複製回Host進行驗證,驗證用的參考影像我們可以重複使用image_data這個buffer,因為先前把影像複製到GPU後他的任就完成了,這邊就是把它的像素修改成我們預期的(100, 50, 150, 255),再把它拿來跟輸出的result_image_data進行比對

記得釋放資源

    g_free(image_data);
    g_free(result_image_data);
    gst_buffer_unref(out_buf);
    gst_harness_teardown(h);

最後就是要記得釋放我們測試過程中分配的資源,然後要留意哪些資源的所有權已經被轉移了,例如sut加入h之後,所有權就轉交到h;in_buf推入h之後所有權會轉交給h,但我們再從h拉取out_buf,此時所有權又轉交回我們身上。因此最後我們需要釋放的資源才是上面看到的這些

結語

要寫出好的元件必須得搭配好的測試,而GStreamer運行時的變數非常複雜,透過GstCheck建立良好的單元測試能夠讓你更精準地排除潛在的Bug,並且每次更新元件時你可以非常有把握的驗證每一項功能,而不是一直運行Pipeline用肉眼看結果


上一篇
[Day 28] 不必畏懼的CUDA Kernel
系列文
深入認識DeepStream,不只是停在執行範例 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言