From 387af9ba495ca995c696213976b39675aaac2680 Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Tue, 14 Jul 2026 19:49:45 -0500 Subject: [PATCH] test: add hardware-independent libuvc regression gate --- .github/workflows/build.yml | 89 ++++++++- .gitignore | 1 + CHANGELOG.ceralive.md | 6 +- CMakeLists.txt | 46 +++++ README.md | 28 ++- docs/evidence/uvc-camera-compat-stability.md | 84 ++++++++ src/device.c | 16 ++ src/stream.c | 10 +- tests/descriptor_assertions.c | 172 ++++++++++++++++ tests/negotiation_assertions.c | 194 +++++++++++++++++++ tests/transfer_assertions.c | 151 +++++++++++++++ 11 files changed, 780 insertions(+), 17 deletions(-) create mode 100644 docs/evidence/uvc-camera-compat-stability.md create mode 100644 tests/descriptor_assertions.c create mode 100644 tests/negotiation_assertions.c create mode 100644 tests/transfer_assertions.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70318de..70dbd0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,13 @@ on: pull_request: branches: ["main"] +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: name: Build (${{ matrix.os }}) @@ -20,14 +27,20 @@ jobs: os: [ubuntu-22.04, ubuntu-24.04] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + + - name: Initialize regression test results + run: | + result_dir="test-results/libuvc-regression-${{ matrix.os }}" + mkdir -p "$result_dir" + printf 'os=%s\nstatus=started\n' "${{ matrix.os }}" > "$result_dir/status.txt" - name: Install dependencies run: | sudo apt-get update -qq sudo apt-get install -y \ build-essential cmake pkg-config \ - libusb-1.0-0-dev libjpeg-dev ccache + libusb-1.0-0-dev libjpeg-dev ccache jq - name: Record compiler identity id: compiler @@ -39,7 +52,7 @@ jobs: uses: actions/cache@v6 with: path: ${{ env.CCACHE_DIR }} - key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.os }}-${{ steps.compiler.outputs.fingerprint }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'include/**', 'src/**', '.github/workflows/build.yml') }} + key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.os }}-${{ steps.compiler.outputs.fingerprint }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'include/**', 'src/**', 'tests/**', '.github/workflows/build.yml') }} restore-keys: | ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.os }}-${{ steps.compiler.outputs.fingerprint }}- @@ -55,6 +68,7 @@ jobs: -DBUILD_SHARED_LIBS=ON \ -DBUILD_EXAMPLE=OFF \ -DBUILD_TEST=OFF \ + -DBUILD_TESTING=OFF \ -DLIBUVC_AUTO_DETACH_KERNEL_DRIVER=ON \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache @@ -62,16 +76,70 @@ jobs: - name: Build run: cmake --build build --parallel - - name: Verify capabilities present + - name: Configure regression tests run: | - echo "=== H.265 support ===" - grep -r "UVC_FRAME_FORMAT_H265" include/ && echo "PASS: H265 enum present" + set -o pipefail + result_dir="test-results/libuvc-regression-${{ matrix.os }}" + cmake -S . -B "$result_dir/build" \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_BUILD_TARGET=Static \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_EXAMPLE=OFF \ + -DBUILD_TEST=OFF \ + -DBUILD_TESTING=ON \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + 2>&1 | tee "$result_dir/configure.log" - echo "=== UVC 1.5 header ===" - grep -r "0x0150" src/ && echo "PASS: UVC 1.5 case present" + - name: Build regression tests + run: | + set -o pipefail + result_dir="test-results/libuvc-regression-${{ matrix.os }}" + cmake --build "$result_dir/build" --parallel \ + 2>&1 | tee "$result_dir/build.log" - echo "=== Configurable auto-detach ===" - grep "LIBUVC_AUTO_DETACH_KERNEL_DRIVER" CMakeLists.txt && echo "PASS: auto-detach option present" + - name: Verify exact regression test inventory + run: | + set -o pipefail + result_dir="test-results/libuvc-regression-${{ matrix.os }}" + ctest --test-dir "$result_dir/build" --show-only=json-v1 \ + > "$result_dir/inventory.json" + jq -e ' + ([.tests[].name] | sort) == ([ + "libuvc.descriptor.degenerate_h26x", + "libuvc.descriptor.h264", + "libuvc.descriptor.h265", + "libuvc.descriptor.truncated_format", + "libuvc.descriptor.truncated_frame", + "libuvc.negotiation.h264", + "libuvc.negotiation.h265", + "libuvc.negotiation.near_match", + "libuvc.negotiation.probe_get_error", + "libuvc.negotiation.probe_set_error", + "libuvc.transfer.retry_failure", + "libuvc.transfer.retry_success", + "libuvc.transfer.terminal_statuses" + ] | sort) and (.tests | length == 13) + ' "$result_dir/inventory.json" \ + 2>&1 | tee "$result_dir/inventory-check.log" + + - name: Run regression tests + run: | + set -o pipefail + result_dir="$GITHUB_WORKSPACE/test-results/libuvc-regression-${{ matrix.os }}" + ctest --test-dir "$result_dir/build" \ + --output-on-failure \ + --output-junit "$result_dir/junit.xml" \ + 2>&1 | tee "$result_dir/ctest.log" + + - name: Upload regression test results + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: libuvc-regression-${{ matrix.os }} + path: test-results/libuvc-regression-${{ matrix.os }}/ + if-no-files-found: error - name: Configure + build (auto-detach OFF — rollback path) run: | @@ -80,6 +148,7 @@ jobs: -DBUILD_SHARED_LIBS=ON \ -DBUILD_EXAMPLE=OFF \ -DBUILD_TEST=OFF \ + -DBUILD_TESTING=OFF \ -DLIBUVC_AUTO_DETACH_KERNEL_DRIVER=OFF \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache diff --git a/.gitignore b/.gitignore index 5f97a27..6c4c4ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Build directory (recommended location) build/ +test-results/ diff --git a/CHANGELOG.ceralive.md b/CHANGELOG.ceralive.md index ce7425c..e03c17a 100644 --- a/CHANGELOG.ceralive.md +++ b/CHANGELOG.ceralive.md @@ -311,8 +311,10 @@ fork-style-adapted commit on `hardening/v0.0.7.3` (branched from `eae7f49`, tag `ceralive-v0.0.7.2`). Every change is either off-by-default or a pure robustness guard — no negotiated default changes and byte-identical streaming behavior for currently-working devices. Each entry cites its **fork-commit SHA** and upstream -provenance. Backlog IDs (A1–A14) refer to the equivalence audit in the plugin -repo's `.omo/evidence/task-1-uvc-camera-compat-stability.md`. +provenance. Backlog IDs (A1–A14) and their durable evidence dispositions are +recorded in `docs/evidence/uvc-camera-compat-stability.md`. The focused +hardware-independent assertions live in `tests/` and are enforced by the +blocking regression gate in `.github/workflows/build.yml`. ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index f7d10bf..e666578 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,8 @@ endif() option(BUILD_EXAMPLE "Build example program" ON) option(BUILD_TEST "Build test program" OFF) +option(BUILD_TESTING "Build hardware-independent regression tests" OFF) +include(CTest) option(ENABLE_UVC_DEBUGGING "Enable UVC debugging" OFF) option(LIBUVC_AUTO_DETACH_KERNEL_DRIVER "Auto-detach the kernel driver (e.g. uvcvideo) when claiming UVC interfaces (CeraLive)" @@ -146,6 +148,50 @@ foreach(target_name IN LISTS UVC_TARGETS) endif() endforeach() +if(BUILD_TESTING) + if(NOT TARGET uvc_static) + message(FATAL_ERROR + "BUILD_TESTING requires CMAKE_BUILD_TARGET=Static or Both") + endif() + if(NOT UNIX OR APPLE OR NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + message(FATAL_ERROR + "The libuvc regression tests require Linux and GNU-compatible --wrap") + endif() + + add_executable(uvc_descriptor_assertions tests/descriptor_assertions.c) + add_executable(uvc_negotiation_assertions tests/negotiation_assertions.c) + add_executable(uvc_transfer_assertions tests/transfer_assertions.c) + + foreach(test_target + uvc_descriptor_assertions + uvc_negotiation_assertions + uvc_transfer_assertions) + target_link_libraries(${test_target} + PRIVATE uvc_static LibUSB::LibUSB ${threads}) + if(JPEG_FOUND) + target_link_libraries(${test_target} PRIVATE JPEG::JPEG) + endif() + endforeach() + + set_property(TARGET uvc_negotiation_assertions APPEND_STRING + PROPERTY LINK_FLAGS " -Wl,--wrap=libusb_control_transfer -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/02-link.map") + set_property(TARGET uvc_transfer_assertions APPEND_STRING + PROPERTY LINK_FLAGS " -Wl,--wrap=libusb_submit_transfer -Wl,--wrap=libusb_free_transfer -Wl,--wrap=free") + + foreach(case_name h264 h265 truncated_format truncated_frame degenerate_h26x) + add_test(NAME libuvc.descriptor.${case_name} + COMMAND uvc_descriptor_assertions --case ${case_name}) + endforeach() + foreach(case_name h264 h265 near_match probe_set_error probe_get_error) + add_test(NAME libuvc.negotiation.${case_name} + COMMAND uvc_negotiation_assertions --case ${case_name}) + endforeach() + foreach(case_name terminal_statuses retry_success retry_failure) + add_test(NAME libuvc.transfer.${case_name} + COMMAND uvc_transfer_assertions --case ${case_name}) + endforeach() +endif() + if(BUILD_EXAMPLE) add_executable(example src/example.c) find_package(Threads) diff --git a/README.md b/README.md index cbe869a..40317d3 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Prerequisites: You will need `libusb` and [CMake](http://www.cmake.org/) install To build, you can just run these shell commands: - git clone https://github.com/libuvc/libuvc + git clone https://github.com/CERALIVE/libuvc cd libuvc mkdir build cd build @@ -49,8 +49,30 @@ To build, you can just run these shell commands: and you're set! If you want to change the build configuration, you can edit `CMakeCache.txt` in the build directory, or use a CMake GUI to make the desired changes. -There is also `BUILD_EXAMPLE` and `BUILD_TEST` options to enable the compilation of `example` and `uvc_test` programs. To use them, replace the `cmake ..` command above with `cmake .. -DBUILD_TEST=ON -DBUILD_EXAMPLE=ON`. -Then you can start them with `./example` and `./uvc_test` respectively. Note that you need OpenCV to build the later (for displaying image). +`BUILD_EXAMPLE` enables the example program. `BUILD_TEST` enables `uvc_test`, an +interactive OpenCV demo that needs a real UVC camera and a display; it is not the +automated test suite. `BUILD_TESTING` enables the default-off, hardware-independent +Linux CTest suite. Configure, build, inspect, and run its static build with: + + cmake -S . -B build/regression \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_BUILD_TARGET=Static \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_EXAMPLE=OFF \ + -DBUILD_TEST=OFF \ + -DBUILD_TESTING=ON + cmake --build build/regression --parallel + ctest --test-dir build/regression --show-only=json-v1 \ + | jq -e '.tests | length == 13' + ctest --test-dir build/regression --output-on-failure + +The 13 cases are grouped as descriptor (5: `h264`, `h265`, +`truncated_format`, `truncated_frame`, `degenerate_h26x`), negotiation (5: +`h264`, `h265`, `near_match`, `probe_set_error`, `probe_get_error`), and +transfer (3: `terminal_statuses`, `retry_success`, `retry_failure`). CI runs +this suite without camera hardware on Ubuntu 22.04 and Ubuntu 24.04. See +`docs/evidence/uvc-camera-compat-stability.md` for its exact scope. ## Developing with libuvc diff --git a/docs/evidence/uvc-camera-compat-stability.md b/docs/evidence/uvc-camera-compat-stability.md new file mode 100644 index 0000000..1ecce47 --- /dev/null +++ b/docs/evidence/uvc-camera-compat-stability.md @@ -0,0 +1,84 @@ +# UVC camera compatibility and stability evidence + +This document keeps the A1–A14 audit disposition reproducible in a standalone +clone. It distinguishes the focused CTest gate from historical sanitizer work +and manual camera/plugin validation. No camera or other USB hardware was used +to create or run the 13-case suite; it does not reproduce physical-camera +behavior, USB timing, or quarantine/thread races. + +## Focused regression gate + +The production code is in `src/device.c` and `src/stream.c`. CMake registers +three fixtures from `tests/`, and `.github/workflows/build.yml` blocks Ubuntu +22.04 and Ubuntu 24.04 jobs on the exact 13-test inventory and its result: + +- `tests/descriptor_assertions.c`: `libuvc.descriptor.h264`, + `libuvc.descriptor.h265`, `libuvc.descriptor.truncated_format`, + `libuvc.descriptor.truncated_frame`, and + `libuvc.descriptor.degenerate_h26x`. +- `tests/negotiation_assertions.c`: `libuvc.negotiation.h264`, + `libuvc.negotiation.h265`, `libuvc.negotiation.near_match`, + `libuvc.negotiation.probe_set_error`, and + `libuvc.negotiation.probe_get_error`. +- `tests/transfer_assertions.c`: `libuvc.transfer.terminal_statuses`, + `libuvc.transfer.retry_success`, and `libuvc.transfer.retry_failure`. + +Only A4 directly overlaps this focused suite: `degenerate_h26x` asserts the +zero buffer-size and invalid default-interval repair introduced by `5df5401`. +The codec, malformed-descriptor, negotiation-error, and transfer-lifecycle +cases protect other production behavior; they must not be read as automated +coverage of the other A-items. + +Before the corresponding production guards were applied, retained red runs +showed `truncated_format` and `truncated_frame` failing because malformed +descriptor lengths were accepted, and `probe_set_error` and `probe_get_error` +failing because control-transfer errors were discarded. The fixes are the +length/order checks in `src/device.c` and error propagation in `src/stream.c`. +With those fixes, all 13 cases pass. This records a red/green disposition, not +a claim that those four defects belong to A1–A14. + +On Linux with CMake, a GNU-compatible C compiler, libusb development headers, +and pthreads, reproduce the green result exactly with: + +```sh +cmake -S . -B build/regression \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_BUILD_TARGET=Static \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_EXAMPLE=OFF \ + -DBUILD_TEST=OFF \ + -DBUILD_TESTING=ON +cmake --build build/regression --parallel +ctest --test-dir build/regression --show-only=json-v1 \ + | jq -e '.tests | length == 13' +ctest --test-dir build/regression --output-on-failure +``` + +Expected green outcome: exactly 13 discovered tests and 13 passes. A red run +returns non-zero and names the failed assertion. Capture local tool versions +with `cmake --version; ctest --version; cc --version; pkg-config --modversion +libusb-1.0`. + +## A1–A14 dispositions + +| Item | Disposition | Durable repository evidence and limits | +|---|---|---| +| A1 | not covered by this focused gate | Alt-setting retry is production code from `3195bbc` in `src/stream.c`; transient real-USB timing remains hardware scope. | +| A2 | not covered by this focused gate | Transfer-count API and zero-submit handling are in `001e8d3`, `include/libuvc/libuvc.h`, and `src/stream.c`; the 13 cases do not exercise stream startup allocation. | +| A3 | historical sanitizer evidence only | Metadata cleanup is in `3195bbc`; teardown leak validation was retained historical sanitizer scope, not this CTest suite. | +| A4 | repository CTest coverage | Descriptor repair is in `5df5401` and `src/device.c`; `libuvc.descriptor.degenerate_h26x` directly asserts both repairs without hardware. Actual DJI descriptors/capture remain hardware scope. | +| A5 | historical sanitizer evidence only | The bounded stop and later quarantine hardening are recorded by `ab49e21`, `dfba86f`, `a1949ae`, `3ede00e`, `2dcca59`, and `71588db`; the thread/timing harness is not part of this focused gate. | +| A6 | not covered by this focused gate | Existing control-interface routing uses parsed interface state in `src/ctrl.c`; no new backport or focused assertion was required. Composite hardware remains manual scope. | +| A7 | not covered by this focused gate | The zero `GET_MAX` payload fallback is in `69c7da8` and `src/stream.c`; the negotiation cases here do not assert it. | +| A8 | not covered by this focused gate | The corrupt-payload superset in A9 subsumed this item in `69c7da8`; packet payload processing is not exercised here. | +| A9 | not covered by this focused gate | Payload bounds/error handling is in `69c7da8` and `src/stream.c`; callback delivery with real packets remains outside this gate. | +| A10 | not covered by this focused gate | Descriptor robustness was already backported in `eae7f49` in `src/device.c`; these descriptor fixtures do not exercise device enumeration. | +| A11 | hardware/plugin validation only | Auto-detach configuration is in `2f32812`, `CMakeLists.txt`, and `src/device.c`; CI builds both option values, but kernel-driver detach behavior requires hardware/system validation. | +| A12 | not covered by this focused gate | Clock preservation is in `9874f4c` in `src/device.c` and `src/stream.c`; the value is not surfaced onto frames and is not asserted here. | +| A13 | historical sanitizer evidence only | Audit disposition in `9874f4c` found the libuvc refcount path already correct and the Android-only path absent; historical enumeration leak checking was not added to this suite. | +| A14 | hardware/plugin validation only | The opt-in double-probe quirk belongs to the consuming plugin, not this repository; its device table and camera validation remain plugin/manual scope. | + +CI and local CTest use synthetic descriptors and wrapped libusb calls. Passing +them establishes deterministic parser, negotiation, and transfer-callback +behavior only. It does not establish end-to-end camera compatibility. diff --git a/src/device.c b/src/device.c index 07dcff8..d213850 100644 --- a/src/device.c +++ b/src/device.c @@ -1453,6 +1453,11 @@ uvc_error_t uvc_parse_vs_frame_format(uvc_streaming_interface_t *stream_if, size_t block_size) { UVC_ENTER(); + if (block_size < 28) { + UVC_EXIT(UVC_ERROR_INVALID_DEVICE); + return UVC_ERROR_INVALID_DEVICE; + } + uvc_format_desc_t *format = calloc(1, sizeof(*format)); format->parent = stream_if; @@ -1568,6 +1573,17 @@ uvc_error_t uvc_parse_vs_frame_frame(uvc_streaming_interface_t *stream_if, UVC_ENTER(); + if (block_size < 26 || !stream_if->format_descs) { + UVC_EXIT(UVC_ERROR_INVALID_DEVICE); + return UVC_ERROR_INVALID_DEVICE; + } + + if ((block[21] == 0 && block_size < 38) || + (block[21] != 0 && block_size < 26 + 4 * (size_t) block[21])) { + UVC_EXIT(UVC_ERROR_INVALID_DEVICE); + return UVC_ERROR_INVALID_DEVICE; + } + format = stream_if->format_descs->prev; frame = calloc(1, sizeof(*frame)); diff --git a/src/stream.c b/src/stream.c index e8e9a47..ebea893 100644 --- a/src/stream.c +++ b/src/stream.c @@ -625,9 +625,15 @@ uvc_error_t uvc_probe_stream_ctrl( uvc_device_handle_t *devh, uvc_stream_ctrl_t *ctrl) { uvc_stream_ctrl_t required_ctrl = *ctrl; + uvc_error_t result; - uvc_query_stream_ctrl( devh, ctrl, 1, UVC_SET_CUR ); - uvc_query_stream_ctrl( devh, ctrl, 1, UVC_GET_CUR ); + result = uvc_query_stream_ctrl(devh, ctrl, 1, UVC_SET_CUR); + if (result != UVC_SUCCESS) + return result; + + result = uvc_query_stream_ctrl(devh, ctrl, 1, UVC_GET_CUR); + if (result != UVC_SUCCESS) + return result; if(!_uvc_stream_params_negotiated(&required_ctrl, ctrl)) { UVC_DEBUG("Unable to negotiate streaming format"); diff --git a/tests/descriptor_assertions.c b/tests/descriptor_assertions.c new file mode 100644 index 0000000..434c697 --- /dev/null +++ b/tests/descriptor_assertions.c @@ -0,0 +1,172 @@ +#include "libuvc/libuvc.h" +#include "libuvc/libuvc_internal.h" + +#include +#include +#include + +uvc_error_t uvc_parse_vs_frame_format(uvc_streaming_interface_t *, + const unsigned char *, size_t); +uvc_error_t uvc_parse_vs_frame_frame(uvc_streaming_interface_t *, + const unsigned char *, size_t); + +#define CHECK(expression) do { \ + if (!(expression)) { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #expression); \ + return EXIT_FAILURE; \ + } \ +} while (0) + +static const unsigned char h264_guid[16] = { + 'H', '2', '6', '4', 0x00, 0x00, 0x10, 0x00, + 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 +}; +static const unsigned char h265_guid[16] = { + 'H', '2', '6', '5', 0x00, 0x00, 0x10, 0x00, + 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 +}; + +static void make_format(unsigned char block[28], const unsigned char guid[16]) { + memset(block, 0, 28); + block[0] = 28; + block[1] = 0x24; + block[2] = UVC_VS_FORMAT_FRAME_BASED; + block[3] = 7; + block[4] = 1; + memcpy(block + 5, guid, 16); + block[21] = 0; + block[22] = 1; +} + +static void put_u32(unsigned char *destination, uint32_t value) { + destination[0] = (unsigned char) value; + destination[1] = (unsigned char) (value >> 8); + destination[2] = (unsigned char) (value >> 16); + destination[3] = (unsigned char) (value >> 24); +} + +static void make_frame(unsigned char block[30], uint32_t buffer_size, + uint32_t default_interval) { + memset(block, 0, 30); + block[0] = 30; + block[1] = 0x24; + block[2] = UVC_VS_FRAME_FRAME_BASED; + block[3] = 3; + block[5] = 0x80; + block[6] = 0x07; + block[7] = 0x38; + block[8] = 0x04; + put_u32(block + 17, default_interval); + block[21] = 1; + put_u32(block + 22, buffer_size); + put_u32(block + 26, 333333); +} + +static void release_stream(uvc_streaming_interface_t *stream_if) { + uvc_format_desc_t *format = stream_if->format_descs; + if (format) { + uvc_frame_desc_t *frame = format->frame_descs; + if (frame) { + free(frame->intervals); + free(frame); + } + free(format); + } + stream_if->format_descs = NULL; +} + +static int check_codec(const unsigned char guid[16]) { + unsigned char block[28]; + uvc_streaming_interface_t stream_if = {0}; + uvc_format_desc_t *format; + + make_format(block, guid); + CHECK(uvc_parse_vs_frame_format(&stream_if, block, sizeof(block)) == UVC_SUCCESS); + format = stream_if.format_descs; + CHECK(format != NULL); + CHECK(format->bDescriptorSubtype == UVC_VS_FORMAT_FRAME_BASED); + CHECK(format->bFormatIndex == 7); + CHECK(format->bNumFrameDescriptors == 1); + CHECK(memcmp(format->guidFormat, guid, 16) == 0); + release_stream(&stream_if); + return EXIT_SUCCESS; +} + +static int check_truncated_format(void) { + unsigned char block[28]; + uvc_streaming_interface_t stream_if = {0}; + uvc_error_t result; + + make_format(block, h264_guid); + result = uvc_parse_vs_frame_format(&stream_if, block, 27); + if (result != UVC_ERROR_INVALID_DEVICE) + fprintf(stderr, "observed=%d required=%d\n", result, + UVC_ERROR_INVALID_DEVICE); + CHECK(result == UVC_ERROR_INVALID_DEVICE); + CHECK(stream_if.format_descs == NULL); + return EXIT_SUCCESS; +} + +static int check_truncated_frame(void) { + unsigned char format_block[28]; + unsigned char frame_block[30]; + uvc_streaming_interface_t stream_if = {0}; + uvc_streaming_interface_t unordered = {0}; + + make_format(format_block, h264_guid); + make_frame(frame_block, 4147200, 333333); + CHECK(uvc_parse_vs_frame_format(&stream_if, format_block, + sizeof(format_block)) == UVC_SUCCESS); + { + uvc_error_t result = uvc_parse_vs_frame_frame(&stream_if, frame_block, 29); + if (result != UVC_ERROR_INVALID_DEVICE) + fprintf(stderr, "observed=%d required=%d\n", result, + UVC_ERROR_INVALID_DEVICE); + CHECK(result == UVC_ERROR_INVALID_DEVICE); + } + CHECK(stream_if.format_descs->frame_descs == NULL); + CHECK(uvc_parse_vs_frame_frame(&unordered, frame_block, + sizeof(frame_block)) == + UVC_ERROR_INVALID_DEVICE); + release_stream(&stream_if); + return EXIT_SUCCESS; +} + +static int check_degenerate(void) { + unsigned char format_block[28]; + unsigned char frame_block[30]; + uvc_streaming_interface_t stream_if = {0}; + uvc_frame_desc_t *frame; + + make_format(format_block, h265_guid); + make_frame(frame_block, 0, 0); + CHECK(uvc_parse_vs_frame_format(&stream_if, format_block, + sizeof(format_block)) == UVC_SUCCESS); + CHECK(uvc_parse_vs_frame_frame(&stream_if, frame_block, + sizeof(frame_block)) == UVC_SUCCESS); + frame = stream_if.format_descs->frame_descs; + CHECK(frame != NULL); + CHECK(frame->wWidth == 1920 && frame->wHeight == 1080); + CHECK(frame->dwMaxVideoFrameBufferSize == 1920U * 1080U * 2U); + CHECK(frame->dwDefaultFrameInterval == 333333); + CHECK(frame->intervals[0] == 333333 && frame->intervals[1] == 0); + release_stream(&stream_if); + return EXIT_SUCCESS; +} + +int main(int argc, char **argv) { + CHECK(argc == 3 && strcmp(argv[1], "--case") == 0); + if (strcmp(argv[2], "h264") == 0) + return check_codec(h264_guid); + if (strcmp(argv[2], "h265") == 0) + return check_codec(h265_guid); + if (strcmp(argv[2], "truncated_format") == 0) + return check_truncated_format(); + if (strcmp(argv[2], "truncated_frame") == 0) + return check_truncated_frame(); + if (strcmp(argv[2], "degenerate_h26x") == 0) + return check_degenerate(); + fprintf(stderr, "unknown case: %s\n", argv[2]); + return EXIT_FAILURE; +} diff --git a/tests/negotiation_assertions.c b/tests/negotiation_assertions.c new file mode 100644 index 0000000..af86329 --- /dev/null +++ b/tests/negotiation_assertions.c @@ -0,0 +1,194 @@ +#include "libuvc/libuvc.h" +#include "libuvc/libuvc_internal.h" + +#include +#include +#include + +#define CHECK(expression) do { \ + if (!(expression)) { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #expression); \ + return EXIT_FAILURE; \ + } \ +} while (0) + +enum mock_mode { + MOCK_SUCCESS, + MOCK_SET_ERROR, + MOCK_GET_ERROR +}; + +static enum mock_mode mode; +static int call_count; +static int unexpected_call; +static uint8_t last_format; +static uint8_t last_frame; +static unsigned char last_interval[4]; + +int __wrap_libusb_control_transfer(libusb_device_handle *device_handle, + uint8_t request_type, uint8_t request, uint16_t value, uint16_t index, + unsigned char *data, uint16_t length, unsigned int timeout) { + (void) device_handle; + (void) timeout; + call_count++; + if (value != (UVC_VS_PROBE_CONTROL << 8) || index != 2 || + (length != 26 && length != 34)) { + unexpected_call = 1; + return LIBUSB_ERROR_INVALID_PARAM; + } + if (request == UVC_SET_CUR) { + if (request_type != 0x21) { + unexpected_call = 1; + return LIBUSB_ERROR_INVALID_PARAM; + } + if (mode == MOCK_SET_ERROR) + return LIBUSB_ERROR_PIPE; + last_format = data[2]; + last_frame = data[3]; + memcpy(last_interval, data + 4, sizeof(last_interval)); + return length; + } + if (request_type != 0xA1 || + (request != UVC_GET_MAX && request != UVC_GET_CUR)) { + unexpected_call = 1; + return LIBUSB_ERROR_INVALID_PARAM; + } + if (request == UVC_GET_CUR && mode == MOCK_GET_ERROR) + return LIBUSB_ERROR_TIMEOUT; + if (request == UVC_GET_CUR || request == UVC_GET_MAX) { + data[2] = request == UVC_GET_CUR ? last_format : 0; + data[3] = request == UVC_GET_CUR ? last_frame : 0; + if (request == UVC_GET_CUR) + memcpy(data + 4, last_interval, sizeof(last_interval)); + return length; + } + unexpected_call = 1; + return LIBUSB_ERROR_INVALID_PARAM; +} + +static void reset_mock(enum mock_mode selected_mode) { + mode = selected_mode; + call_count = 0; + unexpected_call = 0; + last_format = 0; + last_frame = 0; + memset(last_interval, 0, sizeof(last_interval)); +} + +static void make_graph(uvc_device_handle_t *devh, uvc_device_info_t *info, + uvc_streaming_interface_t *stream_if, + uvc_format_desc_t formats[2], + uvc_frame_desc_t frames[2]) { + static const unsigned char guids[2][16] = { + {'H','2','6','4',0,0,0x10,0,0x80,0,0,0xaa,0,0x38,0x9b,0x71}, + {'H','2','6','5',0,0,0x10,0,0x80,0,0,0xaa,0,0x38,0x9b,0x71} + }; + static uint32_t intervals[2][2] = {{333333, 0}, {333333, 0}}; + int i; + + memset(devh, 0, sizeof(*devh)); + memset(info, 0, sizeof(*info)); + memset(stream_if, 0, sizeof(*stream_if)); + memset(formats, 0, sizeof(uvc_format_desc_t) * 2); + memset(frames, 0, sizeof(uvc_frame_desc_t) * 2); + devh->info = info; + devh->claimed = 1U << 2; + info->stream_ifs = stream_if; + stream_if->parent = info; + stream_if->bInterfaceNumber = 2; + stream_if->format_descs = formats; + formats[0].next = &formats[1]; + formats[0].prev = &formats[1]; + formats[1].next = NULL; + formats[1].prev = &formats[0]; + for (i = 0; i < 2; ++i) { + formats[i].parent = stream_if; + formats[i].bFormatIndex = (uint8_t) (4 + i); + memcpy(formats[i].guidFormat, guids[i], 16); + formats[i].frame_descs = &frames[i]; + frames[i].parent = &formats[i]; + frames[i].bFrameIndex = 1; + frames[i].wWidth = 1920; + frames[i].wHeight = 1080; + frames[i].intervals = intervals[i]; + } +} + +static int check_selection(enum uvc_frame_format format, uint8_t expected_index) { + uvc_device_handle_t devh; + uvc_device_info_t info; + uvc_streaming_interface_t stream_if; + uvc_format_desc_t formats[2]; + uvc_frame_desc_t frames[2]; + uvc_stream_ctrl_t ctrl = {0}; + + make_graph(&devh, &info, &stream_if, formats, frames); + reset_mock(MOCK_SUCCESS); + CHECK(uvc_get_stream_ctrl_format_size(&devh, &ctrl, format, + 1920, 1080, 30) == UVC_SUCCESS); + CHECK(ctrl.bInterfaceNumber == 2); + CHECK(ctrl.bFormatIndex == expected_index); + CHECK(ctrl.bFrameIndex == 1); + CHECK(ctrl.dwFrameInterval == 333333); + CHECK(call_count == 3 && unexpected_call == 0); + return EXIT_SUCCESS; +} + +static int check_near_match(void) { + uvc_device_handle_t devh; + uvc_device_info_t info; + uvc_streaming_interface_t stream_if; + uvc_format_desc_t formats[2]; + uvc_frame_desc_t frames[2]; + uvc_stream_ctrl_t ctrl = {0}; + + make_graph(&devh, &info, &stream_if, formats, frames); + formats[0].guidFormat[15] ^= 1; + reset_mock(MOCK_SUCCESS); + CHECK(uvc_get_stream_ctrl_format_size(&devh, &ctrl, UVC_FRAME_FORMAT_H264, + 1920, 1080, 30) == + UVC_ERROR_INVALID_MODE); + CHECK(call_count == 0 && unexpected_call == 0); + return EXIT_SUCCESS; +} + +static int check_probe_error(enum mock_mode selected_mode, + uvc_error_t expected) { + uvc_device_handle_t devh; + uvc_device_info_t info; + uvc_stream_ctrl_t ctrl = {0}; + + memset(&devh, 0, sizeof(devh)); + memset(&info, 0, sizeof(info)); + devh.info = &info; + ctrl.bInterfaceNumber = 2; + ctrl.bFormatIndex = 4; + ctrl.bFrameIndex = 1; + reset_mock(selected_mode); + { + uvc_error_t result = uvc_probe_stream_ctrl(&devh, &ctrl); + if (result != expected) + fprintf(stderr, "observed=%d required=%d\n", result, expected); + CHECK(result == expected); + } + CHECK(unexpected_call == 0); + CHECK(call_count == (selected_mode == MOCK_SET_ERROR ? 1 : 2)); + return EXIT_SUCCESS; +} + +int main(int argc, char **argv) { + CHECK(argc == 3 && strcmp(argv[1], "--case") == 0); + if (strcmp(argv[2], "h264") == 0) + return check_selection(UVC_FRAME_FORMAT_H264, 4); + if (strcmp(argv[2], "h265") == 0) + return check_selection(UVC_FRAME_FORMAT_H265, 5); + if (strcmp(argv[2], "near_match") == 0) + return check_near_match(); + if (strcmp(argv[2], "probe_set_error") == 0) + return check_probe_error(MOCK_SET_ERROR, LIBUSB_ERROR_PIPE); + if (strcmp(argv[2], "probe_get_error") == 0) + return check_probe_error(MOCK_GET_ERROR, LIBUSB_ERROR_TIMEOUT); + fprintf(stderr, "unknown case: %s\n", argv[2]); + return EXIT_FAILURE; +} diff --git a/tests/transfer_assertions.c b/tests/transfer_assertions.c new file mode 100644 index 0000000..9543c9b --- /dev/null +++ b/tests/transfer_assertions.c @@ -0,0 +1,151 @@ +#include "libuvc/libuvc.h" +#include "libuvc/libuvc_internal.h" + +#include +#include +#include + +void LIBUSB_CALL _uvc_stream_callback(struct libusb_transfer *transfer); +void __real_libusb_free_transfer(struct libusb_transfer *transfer); +void __real_free(void *pointer); + +#define CHECK(expression) do { \ + if (!(expression)) { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #expression); \ + return EXIT_FAILURE; \ + } \ +} while (0) + +static struct libusb_transfer *expected_transfer; +static void *expected_buffer; +static int submit_result; +static int submit_calls; +static int transfer_free_calls; +static int buffer_free_calls; +static int unexpected_call; +static int observe_free_calls; + +int __wrap_libusb_submit_transfer(struct libusb_transfer *transfer) { + submit_calls++; + if (transfer != expected_transfer) + unexpected_call = 1; + return submit_result; +} + +void __wrap_libusb_free_transfer(struct libusb_transfer *transfer) { + transfer_free_calls++; + if (transfer != expected_transfer) + unexpected_call = 1; + __real_libusb_free_transfer(transfer); +} + +void __wrap_free(void *pointer) { + if (observe_free_calls) { + if (pointer == expected_buffer) + buffer_free_calls++; + else { + unexpected_call = 1; + return; + } + } + __real_free(pointer); +} + +static void reset_mock(struct libusb_transfer *transfer, void *buffer, + int result) { + expected_transfer = transfer; + expected_buffer = buffer; + submit_result = result; + submit_calls = 0; + transfer_free_calls = 0; + buffer_free_calls = 0; + unexpected_call = 0; + observe_free_calls = 0; +} + +static int run_status(enum libusb_transfer_status status, int running, + int result, int expected_submits, int expected_frees) { + uvc_stream_handle_t stream; + struct libusb_transfer *transfer; + unsigned char *buffer; + + memset(&stream, 0, sizeof(stream)); + CHECK(pthread_mutex_init(&stream.cb_mutex, NULL) == 0); + CHECK(pthread_cond_init(&stream.cb_cond, NULL) == 0); + transfer = libusb_alloc_transfer(0); + CHECK(transfer != NULL); + buffer = malloc(8); + CHECK(buffer != NULL); + transfer->user_data = &stream; + transfer->buffer = buffer; + transfer->status = status; + stream.running = (uint8_t) running; + stream.transfers[3] = transfer; + reset_mock(transfer, buffer, result); + + observe_free_calls = 1; + _uvc_stream_callback(transfer); + observe_free_calls = 0; + + CHECK(submit_calls == expected_submits); + CHECK(transfer_free_calls == expected_frees); + CHECK(buffer_free_calls == expected_frees); + CHECK(unexpected_call == 0); + CHECK((stream.transfers[3] == NULL) == (expected_frees != 0)); + if (!expected_frees) { + __real_free(buffer); + __real_libusb_free_transfer(transfer); + } + CHECK(pthread_cond_destroy(&stream.cb_cond) == 0); + CHECK(pthread_mutex_destroy(&stream.cb_mutex) == 0); + return EXIT_SUCCESS; +} + +static int check_terminal_statuses(void) { + static const enum libusb_transfer_status statuses[] = { + LIBUSB_TRANSFER_CANCELLED, + LIBUSB_TRANSFER_ERROR, + LIBUSB_TRANSFER_NO_DEVICE + }; + size_t i; + for (i = 0; i < sizeof(statuses) / sizeof(statuses[0]); ++i) + CHECK(run_status(statuses[i], 1, 0, 0, 1) == EXIT_SUCCESS); + return EXIT_SUCCESS; +} + +static int check_retry_success(void) { + static const enum libusb_transfer_status statuses[] = { + LIBUSB_TRANSFER_TIMED_OUT, + LIBUSB_TRANSFER_STALL, + LIBUSB_TRANSFER_OVERFLOW + }; + size_t i; + for (i = 0; i < sizeof(statuses) / sizeof(statuses[0]); ++i) + CHECK(run_status(statuses[i], 1, 0, 1, 0) == EXIT_SUCCESS); + return EXIT_SUCCESS; +} + +static int check_retry_failure(void) { + static const enum libusb_transfer_status statuses[] = { + LIBUSB_TRANSFER_TIMED_OUT, + LIBUSB_TRANSFER_STALL, + LIBUSB_TRANSFER_OVERFLOW + }; + size_t i; + for (i = 0; i < sizeof(statuses) / sizeof(statuses[0]); ++i) + CHECK(run_status(statuses[i], 1, LIBUSB_ERROR_IO, 1, 1) == EXIT_SUCCESS); + return EXIT_SUCCESS; +} + +int main(int argc, char **argv) { + CHECK(argc == 3 && strcmp(argv[1], "--case") == 0); + if (strcmp(argv[2], "terminal_statuses") == 0) + return check_terminal_statuses(); + if (strcmp(argv[2], "retry_success") == 0) + return check_retry_success(); + if (strcmp(argv[2], "retry_failure") == 0) + return check_retry_failure(); + fprintf(stderr, "unknown case: %s\n", argv[2]); + return EXIT_FAILURE; +}