diff --git a/.github/workflows/vendor-fixtures.yml b/.github/workflows/vendor-fixtures.yml index b3b2ff10..f8cabcbc 100644 --- a/.github/workflows/vendor-fixtures.yml +++ b/.github/workflows/vendor-fixtures.yml @@ -38,6 +38,8 @@ jobs: sh -n application/qmodem/tests/test_core_cmds.sh bash -n application/qmodem/tests/test_collect_encryption.sh bash -n application/qmodem/tests/test_fixture_profiles.sh + bash -n application/qmodem/tests/test_fixture_response_queue.sh + bash -n application/qmodem/tests/test_import_testcases.sh sh -n application/qmodem/tests/test_vendor_cmds_boundary.sh bash -n application/qmodem/tests/test_fixture_collection_bytes.sh bash -n application/qmodem/tests/test_recognition_collection.sh @@ -51,6 +53,8 @@ jobs: application/qmodem/tests/test_recognition_collection.sh application/qmodem/tests/test_recognition_fixtures.sh application/qmodem/tests/test_fixture_profiles.sh + application/qmodem/tests/test_fixture_response_queue.sh + application/qmodem/tests/test_import_testcases.sh QMODEM_TEST_SEAL_BIN=/tmp/qmodem-seal \ application/qmodem/tests/test_collect_encryption.sh - name: Replay vendor fixtures diff --git a/application/qmodem/files/etc/config/qmodem b/application/qmodem/files/etc/config/qmodem index 8053603a..947c5de1 100644 --- a/application/qmodem/files/etc/config/qmodem +++ b/application/qmodem/files/etc/config/qmodem @@ -16,3 +16,4 @@ config main 'main' option usage_stats_nvram_interval '300' #collect AT command fixtures to /tmp/qmodem/testcases for qmodem_collect pack option testcase_collect '0' + option testcase_scenario 'default' diff --git a/application/qmodem/files/usr/sbin/qmodem_collect b/application/qmodem/files/usr/sbin/qmodem_collect index 9dc7d9a8..c7caa948 100755 --- a/application/qmodem/files/usr/sbin/qmodem_collect +++ b/application/qmodem/files/usr/sbin/qmodem_collect @@ -88,12 +88,14 @@ resolve_recognition_fixture() cmd_status() { - local seal_bin recipient_id + local seal_bin recipient_id scenario if collect_enabled; then echo "testcase_collect: on" else echo "testcase_collect: off (enable: uci set qmodem.main.testcase_collect=1 && uci commit qmodem)" fi + scenario=$(uci -q get qmodem.main.testcase_scenario 2>/dev/null) + echo "testcase_scenario: ${scenario:-default}" if [ -d "$COLLECT_DIR" ]; then echo "collect dir: $COLLECT_DIR ($(find "$COLLECT_DIR" -name '*.json' 2>/dev/null | wc -l) fixture files)" else @@ -135,7 +137,7 @@ cmd_pack() } local work out_file file count timestamp inner_file seal_work seal_bin local rel resolved target_rel expected_vendor expected_platform expected_model prepared - local recipient recipient_id seal_output review_key wrapped_key_hex + local recipient recipient_id seal_output review_key wrapped_key_hex response_count response_index update_file work=$(mktemp -d) || exit 1 PACK_WORK="$work" count=0 @@ -148,10 +150,20 @@ cmd_pack() expected_platform=$(printf '%s' "$resolved" | cut -f3) expected_model=$(printf '%s' "$resolved" | cut -f4) prepared="$file" - if [ -n "$expected_vendor" ]; then + if [ "${target_rel#*/expected/}" = "$target_rel" ]; then prepared="$work/.fixture.$count.json" - jq --arg vendor "$expected_vendor" --arg platform "$expected_platform" --arg model "$expected_model" \ - '.expected_identity={vendor:$vendor, platform:$platform, model:$model}' "$file" > "$prepared" || exit 1 + jq --arg vendor "$expected_vendor" --arg platform "$expected_platform" --arg model "$expected_model" ' + if has("responses") then . + else .responses=[{response_hex:(.response_hex // ""), response:.response, + rc:(.rc // 0), timestamp:(.timestamp // "")}] + | del(.response_hex, .response, .rc, .timestamp) + end + | .responses |= map(if .response == null then del(.response) else . end) + | .responses |= to_entries | .responses |= map( + .value + {scenario:(.value.scenario // "default"), sequence:(.value.sequence // .key), + capture_sequence:(.value.capture_sequence // .value.sequence // .key)}) + | if $vendor != "" then .expected_identity={vendor:$vendor, platform:$platform, model:$model} + else . end' "$file" > "$prepared" || exit 1 fi mkdir -p "$work/$(dirname "$target_rel")" if [ "$raw" = "1" ]; then @@ -168,28 +180,33 @@ cmd_pack() cmd_masked=$(jq -r '.command // ""' "$prepared" | mask_digits) raw_response="$work/.response.raw" sanitized_response="$work/.response.sanitized" - resp_hex=$(jq -r '.response_hex // empty' "$prepared") - if [ -n "$resp_hex" ]; then - printf '%s' "$resp_hex" | xxd -r -p > "$raw_response" || { + jq --arg c "$cmd_masked" '.command=$c | .sanitized=true' "$prepared" > "$work/$target_rel" || exit 1 + response_count=$(jq '.responses | length' "$prepared") + response_index=0 + while [ "$response_index" -lt "$response_count" ]; do + resp_hex=$(jq -r --argjson i "$response_index" '.responses[$i].response_hex // empty' "$prepared") + if [ -n "$resp_hex" ]; then + printf '%s' "$resp_hex" | xxd -r -p > "$raw_response" || { + rm -rf "$work" + echo "invalid response_hex in fixture: $file response[$response_index]" >&2 + exit 1 + } + else + jq -j --argjson i "$response_index" '.responses[$i].response // ""' "$prepared" > "$raw_response" + fi + mask_digits < "$raw_response" > "$sanitized_response" + resp_hex=$(xxd -p "$sanitized_response" | tr -d '\n') || { rm -rf "$work" - echo "invalid response_hex in fixture: $file" >&2 + echo "failed to encode fixture response: $file response[$response_index]" >&2 exit 1 } - else - jq -j '.response // ""' "$prepared" > "$raw_response" - fi - mask_digits < "$raw_response" > "$sanitized_response" - resp_hex=$(xxd -p "$sanitized_response" | tr -d '\n') || { - rm -rf "$work" - echo "failed to encode fixture response: $file" >&2 - exit 1 - } - jq --arg c "$cmd_masked" --arg h "$resp_hex" \ - '.command=$c | .response_hex=$h | del(.response) | .sanitized=true' "$prepared" > "$work/$target_rel" || { - rm -rf "$work" - echo "failed to sanitize fixture: $file" >&2 - exit 1 - } + update_file="$work/.sanitized.$count.json" + jq --argjson i "$response_index" --arg h "$resp_hex" \ + '.responses[$i].response_hex=$h | del(.responses[$i].response)' \ + "$work/$target_rel" > "$update_file" || exit 1 + mv "$update_file" "$work/$target_rel" + response_index=$((response_index + 1)) + done rm -f "$raw_response" "$sanitized_response" fi [ "$prepared" = "$file" ] || rm -f "$prepared" diff --git a/application/qmodem/files/usr/share/qmodem/modem_ctrl.sh b/application/qmodem/files/usr/share/qmodem/modem_ctrl.sh index 543e95ea..d7bc8280 100755 --- a/application/qmodem/files/usr/share/qmodem/modem_ctrl.sh +++ b/application/qmodem/files/usr/share/qmodem/modem_ctrl.sh @@ -332,7 +332,7 @@ case $method in esac #optionally keep a golden snapshot of the final JSON for fixture tests if qmodem_testcase_collect_enabled; then - expected_dir="$(qmodem_testcase_profile_dir)/expected" + expected_dir="$(qmodem_testcase_profile_dir)/expected/$(qmodem_testcase_scenario)" mkdir -p "$expected_dir" 2>/dev/null json_dump | tee "$expected_dir/$method.json" else diff --git a/application/qmodem/files/usr/share/qmodem/modem_util.sh b/application/qmodem/files/usr/share/qmodem/modem_util.sh index e0396017..b8c08092 100755 --- a/application/qmodem/files/usr/share/qmodem/modem_util.sh +++ b/application/qmodem/files/usr/share/qmodem/modem_util.sh @@ -31,6 +31,13 @@ qmodem_testcase_path_segment() printf '%s' "$slug" } +qmodem_testcase_scenario() +{ + local scenario="${QMODEM_TESTCASE_SCENARIO:-}" + [ -n "$scenario" ] || scenario=$(uci -q get qmodem.main.testcase_scenario 2>/dev/null) + qmodem_testcase_path_segment "${scenario:-default}" default +} + qmodem_testcase_profile_dir() { local collect_dir="${QMODEM_COLLECT_DIR:-/tmp/qmodem/testcases}" @@ -61,7 +68,7 @@ qmodem_record_testcase_file() local vendor_name="${vendor:-${manufacturer:-core}}" local platform_name="${platform:-unknown}" local model_name="${QMODEM_TESTCASE_MODEL:-}" dir phase=vendor - local slug hash file + local slug hash file update_file timestamp scenario lock_dir attempts [ -n "$model_name" ] || model_name=$(uci -q get "qmodem.${config_section:-}.name" 2>/dev/null) [ -n "$model_name" ] || model_name="unknown" if [ "$vendor_name" = "core" ] || [ "$vendor_name" = "unknown" ] || [ "$model_name" = "unknown" ]; then @@ -73,6 +80,43 @@ qmodem_record_testcase_file() hash=$(printf '%s' "$atcmd" | md5sum | cut -c1-8) file="${dir}/${slug}-${hash}.json" response_hex=$(xxd -p "$response_file" | tr -d '\n') || return 0 + timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) + scenario=$(qmodem_testcase_scenario) + lock_dir="${file}.lock" + attempts=0 + while ! mkdir "$lock_dir" 2>/dev/null; do + attempts=$((attempts + 1)) + [ "$attempts" -lt 100 ] || return 0 + sleep 0.05 + done + update_file=$(mktemp "${dir}/.fixture.XXXXXX") || { + rmdir "$lock_dir" 2>/dev/null + return 0 + } + if [ -f "$file" ]; then + jq --arg response_hex "$response_hex" --argjson rc "$rc" --arg timestamp "$timestamp" \ + --arg scenario "$scenario" ' + if has("responses") then . + else .responses=[{response_hex:(.response_hex // ""), rc:(.rc // 0), + timestamp:(.timestamp // ""), scenario:"default", sequence:0}] + | del(.response_hex, .response, .rc, .timestamp) + end + | .responses |= map(if .response == null then del(.response) else . end) + | .responses |= to_entries | .responses |= map( + .value + {scenario:(.value.scenario // "default"), sequence:(.value.sequence // .key), + capture_sequence:(.value.capture_sequence // .value.sequence // .key)}) + | .responses |= map(.value // .) + | ([.responses[] | select(.scenario == $scenario) | .sequence] | max // -1) as $last + | .responses += [{response_hex:$response_hex, rc:$rc, timestamp:$timestamp, + scenario:$scenario, sequence:($last + 1), capture_sequence:($last + 1)}]' "$file" > "$update_file" 2>/dev/null || { + rm -f "$update_file" + rmdir "$lock_dir" 2>/dev/null + return 0 + } + mv "$update_file" "$file" 2>/dev/null || rm -f "$update_file" + rmdir "$lock_dir" 2>/dev/null + return 0 + fi jq -n \ --arg vendor "$vendor_name" \ --arg platform "$platform_name" \ @@ -81,13 +125,16 @@ qmodem_record_testcase_file() --arg response_hex "$response_hex" \ --arg tool "$tool" \ --arg phase "$phase" \ + --arg scenario "$scenario" \ --arg config_section "${config_section:-unknown}" \ --argjson rc "$rc" \ - --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --arg timestamp "$timestamp" \ '{vendor:$vendor, platform:$platform, model:$model, phase:$phase, - config_section:$config_section, command:$command, - response_hex:$response_hex, tool:$tool, rc:$rc, timestamp:$timestamp}' \ - > "$file" 2>/dev/null || rm -f "$file" + config_section:$config_section, command:$command, tool:$tool, + responses:[{response_hex:$response_hex, rc:$rc, timestamp:$timestamp, + scenario:$scenario, sequence:0, capture_sequence:0}]}' \ + > "$update_file" 2>/dev/null && mv "$update_file" "$file" 2>/dev/null || rm -f "$update_file" + rmdir "$lock_dir" 2>/dev/null } at() diff --git a/application/qmodem/tests/lib/at_fixture.sh b/application/qmodem/tests/lib/at_fixture.sh index cd45e424..21c6bb20 100644 --- a/application/qmodem/tests/lib/at_fixture.sh +++ b/application/qmodem/tests/lib/at_fixture.sh @@ -1,5 +1,5 @@ # Replay recorded fixtures instead of touching hardware. -# Requires FIXTURE_LOOKUP: a dir with .response / .rc files. +# Requires FIXTURE_LOOKUP: ordered responses grouped by command hash and scenario. at() { _fixture_send at "$@"; } fastat() { _fixture_send fastat "$@"; } @@ -8,12 +8,19 @@ _fixture_send() { _fs_cmd=$3 _fs_h=$(printf '%s' "$_fs_cmd" | md5sum | cut -c1-8) - if [ -f "$FIXTURE_LOOKUP/$_fs_h.response" ]; then - cat "$FIXTURE_LOOKUP/$_fs_h.response" + _fs_cursor="$FIXTURE_LOOKUP/cursors/$_fs_h" + _fs_index=0 + [ ! -f "$_fs_cursor" ] || _fs_index=$(cat "$_fs_cursor") + _fs_variant="$FIXTURE_LOOKUP/$_fs_h.responses/${FIXTURE_SCENARIO:-default}/$_fs_index" + if [ -f "$_fs_variant.response" ]; then + printf '%s\n' "$_fs_h" >> "$FIXTURE_LOOKUP/hits.log" + printf '%s\n' "$((_fs_index + 1))" > "$_fs_cursor" + cat "$_fs_variant.response" _fs_rc=0 - [ -f "$FIXTURE_LOOKUP/$_fs_h.rc" ] && _fs_rc=$(cat "$FIXTURE_LOOKUP/$_fs_h.rc") + [ -f "$_fs_variant.rc" ] && _fs_rc=$(cat "$_fs_variant.rc") return "$_fs_rc" fi - echo "$_fs_cmd" >> "$FIXTURE_LOOKUP/misses.log" - return 0 + printf '%s\t%s\t%s\n' "${FIXTURE_SCENARIO:-default}" "$_fs_index" "$_fs_cmd" \ + >> "$FIXTURE_LOOKUP/misses.log" + return 127 } diff --git a/application/qmodem/tests/test_collect_encryption.sh b/application/qmodem/tests/test_collect_encryption.sh index dad90793..7fb77b2e 100755 --- a/application/qmodem/tests/test_collect_encryption.sh +++ b/application/qmodem/tests/test_collect_encryption.sh @@ -20,9 +20,11 @@ trap cleanup EXIT profile=quectel/qualcomm/rm500q-ae-9f94df3c mkdir -p "$test_dir/fixtures/$profile" response_hex=$(printf 'AT+CGSN\r\n861234567890123\r\nOK\r\n\r\n' | xxd -p | tr -d '\n') -jq -n --arg h "$response_hex" \ +response_hex_2=$(printf 'AT+CGSN\r\n869876543210987\r\nOK\r\n\r\n' | xxd -p | tr -d '\n') +jq -n --arg h "$response_hex" --arg h2 "$response_hex_2" \ '{vendor:"quectel",platform:"qualcomm",model:"RM500Q-AE", - command:"AT+CGSN",response_hex:$h,tool:"at",rc:0}' \ + command:"AT+CGSN",tool:"at", + responses:[{response_hex:$h,rc:0},{response_hex:$h2,rc:0}]}' \ > "$test_dir/fixtures/$profile/cgsn.json" plain_output=$(QMODEM_COLLECT_DIR="$test_dir/fixtures" \ @@ -66,8 +68,11 @@ owner_review=$(printf '%s\n' "$token" | "$seal_bin" review-key --token-stdin \ printf '%s\n' "$review_key" | "$seal_bin" decrypt --review-key --token-stdin \ --input "$encrypted_file" --output "$test_dir/review.tar.gz" tar -xOzf "$test_dir/review.tar.gz" "./$profile/cgsn.json" \ - | jq -r '.response_hex' | xxd -r -p \ + | jq -r '.responses[0].response_hex' | xxd -r -p \ | cmp - <(printf 'AT+CGSN\r\n860000000000023\r\nOK\r\n\r\n') +tar -xOzf "$test_dir/review.tar.gz" "./$profile/cgsn.json" \ + | jq -r '.responses[1].response_hex' | xxd -r -p \ + | cmp - <(printf 'AT+CGSN\r\n860000000000087\r\nOK\r\n\r\n') forced_output=$(QMODEM_COLLECT_DIR="$test_dir/fixtures" QMODEM_SEAL_BIN="$seal_bin" \ "$collect_bin" pack --unencrypted 2>&1) diff --git a/application/qmodem/tests/test_fixture_collection_bytes.sh b/application/qmodem/tests/test_fixture_collection_bytes.sh index e38dfc6e..3b6e1c85 100755 --- a/application/qmodem/tests/test_fixture_collection_bytes.sh +++ b/application/qmodem/tests/test_fixture_collection_bytes.sh @@ -40,9 +40,9 @@ rc=$? [ "$rc" -eq 7 ] cmp "$expected" "$actual" fixture=$(find "$QMODEM_COLLECT_DIR/quectel" -name '*BYTECHECK*.json') -fixture_hex_decode "$(jq -r '.response_hex' "$fixture")" > "$recorded" +fixture_hex_decode "$(jq -r '.responses[0].response_hex' "$fixture")" > "$recorded" cmp "$expected" "$recorded" -[ "$(jq -r '.rc' "$fixture")" -eq 7 ] +[ "$(jq -r '.responses[0].rc' "$fixture")" -eq 7 ] [ "$(jq -r '.platform' "$fixture")" = qualcomm ] [ "$(jq -r '.model' "$fixture")" = RM500Q-AE ] case "$fixture" in */quectel/qualcomm/rm500q-ae-9f94df3c/*) ;; *) exit 1 ;; esac @@ -53,8 +53,41 @@ rc=$? [ "$rc" -eq 9 ] cmp "$expected" "$actual" fixture=$(find "$QMODEM_COLLECT_DIR/quectel" -name '*FASTBYTECHECK*.json') -fixture_hex_decode "$(jq -r '.response_hex' "$fixture")" > "$recorded" +fixture_hex_decode "$(jq -r '.responses[0].response_hex' "$fixture")" > "$recorded" cmp "$expected" "$recorded" -[ "$(jq -r '.rc' "$fixture")" -eq 9 ] +[ "$(jq -r '.responses[0].rc' "$fixture")" -eq 9 ] +[ "$(jq -r '.responses[0].scenario' "$fixture")" = default ] +[ "$(jq -r '.responses[0].sequence' "$fixture")" -eq 0 ] +[ "$(jq -r '.responses[0].capture_sequence' "$fixture")" -eq 0 ] + +variant=ONE +tom_modem() +{ + printf '%s\r\nOK\r\n' "$variant" +} +at /dev/ttyUSB2 'AT+VARIANT' >/dev/null +variant=TWO +at /dev/ttyUSB2 'AT+VARIANT' >/dev/null +at /dev/ttyUSB2 'AT+VARIANT' >/dev/null +QMODEM_TESTCASE_SCENARIO=sim-absent +export QMODEM_TESTCASE_SCENARIO +at /dev/ttyUSB2 'AT+VARIANT' >/dev/null +fixture=$(find "$QMODEM_COLLECT_DIR/quectel" -name '*VARIANT*.json') +[ "$(jq '.responses | length' "$fixture")" -eq 4 ] +[ "$(jq -r '.responses[0].response_hex' "$fixture")" != "$(jq -r '.responses[1].response_hex' "$fixture")" ] +[ "$(jq -r '[.responses[] | select(.scenario == "default") | .sequence] | join(" ")' "$fixture")" = '0 1 2' ] +[ "$(jq -r '[.responses[] | select(.scenario == "sim-absent") | .sequence] | join(" ")' "$fixture")" = '0' ] + +QMODEM_TESTCASE_SCENARIO=parallel +export QMODEM_TESTCASE_SCENARIO +for n in 0 1 2 3 4 5 6 7; do + response_file="$QMODEM_COLLECT_DIR/parallel-$n.response" + printf 'RESPONSE-%s\r\n' "$n" > "$response_file" + (qmodem_record_testcase_file at 'AT+PARALLEL' "$response_file" 0) & +done +wait +fixture=$(find "$QMODEM_COLLECT_DIR/quectel" -name '*PARALLEL*.json') +[ "$(jq '.responses | length' "$fixture")" -eq 8 ] +[ "$(jq -r '[.responses[].sequence] | sort | join(" ")' "$fixture")" = '0 1 2 3 4 5 6 7' ] echo 'fixture collection byte tests passed' diff --git a/application/qmodem/tests/test_fixture_profiles.sh b/application/qmodem/tests/test_fixture_profiles.sh index f39665b1..3b3a2106 100755 --- a/application/qmodem/tests/test_fixture_profiles.sh +++ b/application/qmodem/tests/test_fixture_profiles.sh @@ -8,20 +8,33 @@ trap 'rm -rf "$test_root"' EXIT write_fixture() { - local platform=$1 model=$2 model_dir=$3 response=$4 dir hex + local platform=$1 model=$2 model_dir=$3 response=$4 response2=${5:-} dir hex hex2 dir="$test_root/testcases/quectel/$platform/$model_dir" mkdir -p "$dir" hex=$(printf '%s' "$response" | xxd -p | tr -d '\n') - jq -n --arg platform "$platform" --arg model "$model" --arg hex "$hex" \ - '{vendor:"quectel",platform:$platform,model:$model,command:"AT+CGSN", - response_hex:$hex,tool:"at",rc:0}' > "$dir/AT_CGSN.json" + hex2=$(printf '%s' "$response2" | xxd -p | tr -d '\n') + jq -n --arg platform "$platform" --arg model "$model" --arg hex "$hex" --arg hex2 "$hex2" \ + '{vendor:"quectel",platform:$platform,model:$model,command:"AT+CGSN",tool:"at", + responses:([{response_hex:$hex,rc:0,timestamp:"test",scenario:"default",sequence:0,capture_sequence:0}] + + (if $hex2 == "" then [] else [{response_hex:$hex2,rc:0,timestamp:"test",scenario:"roaming",sequence:0,capture_sequence:0}] end))}' \ + > "$dir/AT_CGSN.json" } -write_fixture qualcomm RM500Q-AE rm500q-ae-9f94df3c $'QUALCOMM\r\nOK\r\n' +write_fixture qualcomm RM500Q-AE rm500q-ae-9f94df3c \ + $'AT+CGSN\r\r\n860000000000012\r\n\r\nOK\r\n' \ + $'AT+CGSN\r\r\n860000000000013\r\n\r\nOK\r\n' write_fixture unisoc UDX710 udx710-861f4136 $'UNISOC\r\nOK\r\n' +mkdir -p "$test_root/testcases/quectel/qualcomm/rm500q-ae-9f94df3c/expected/default" +mkdir -p "$test_root/testcases/quectel/qualcomm/rm500q-ae-9f94df3c/expected/roaming" +jq -n '{result:{},imei:"860000000000012"}' \ + > "$test_root/testcases/quectel/qualcomm/rm500q-ae-9f94df3c/expected/default/get_imei.json" +jq -n '{result:{},imei:"860000000000013"}' \ + > "$test_root/testcases/quectel/qualcomm/rm500q-ae-9f94df3c/expected/roaming/get_imei.json" output=$(QMODEM_TEST_REPO_ROOT="$test_root" "$PACKAGE_DIR/tests/test_vendor_fixtures.sh") -printf '%s\n' "$output" | grep -q 'quectel/qualcomm/rm500q-ae-9f94df3c: replay passed' -printf '%s\n' "$output" | grep -q 'quectel/unisoc/udx710-861f4136: replay passed' +printf '%s\n' "$output" | grep -q 'quectel/qualcomm/rm500q-ae-9f94df3c/default: replay passed' +printf '%s\n' "$output" | grep -q 'qualcomm/rm500q-ae-9f94df3c/default: get_imei matches golden snapshot' +printf '%s\n' "$output" | grep -q 'qualcomm/rm500q-ae-9f94df3c/roaming: get_imei matches golden snapshot' +printf '%s\n' "$output" | grep -q 'quectel/unisoc/udx710-861f4136/default: replay passed' echo 'fixture profile isolation tests passed' diff --git a/application/qmodem/tests/test_fixture_response_queue.sh b/application/qmodem/tests/test_fixture_response_queue.sh new file mode 100755 index 00000000..1c2d75dc --- /dev/null +++ b/application/qmodem/tests/test_fixture_response_queue.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# A command consumes ordered responses within one scenario without leaking state. +set -u + +TESTS_DIR=$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +FIXTURE_LOOKUP=$(mktemp -d) +trap 'rm -rf "$FIXTURE_LOOKUP"' EXIT + +command='AT+STATE?' +hash=$(printf '%s' "$command" | md5sum | cut -c1-8) +mkdir -p "$FIXTURE_LOOKUP/$hash.responses/registered" \ + "$FIXTURE_LOOKUP/$hash.responses/sim-absent" "$FIXTURE_LOOKUP/cursors" +printf 'REGISTERING\r\n' > "$FIXTURE_LOOKUP/$hash.responses/registered/0.response" +printf 'REGISTERED\r\n' > "$FIXTURE_LOOKUP/$hash.responses/registered/1.response" +printf 'NO SIM\r\n' > "$FIXTURE_LOOKUP/$hash.responses/sim-absent/0.response" +printf '0\n' > "$FIXTURE_LOOKUP/$hash.responses/registered/0.rc" +printf '0\n' > "$FIXTURE_LOOKUP/$hash.responses/registered/1.rc" +printf '3\n' > "$FIXTURE_LOOKUP/$hash.responses/sim-absent/0.rc" + +export FIXTURE_LOOKUP +. "$TESTS_DIR/lib/at_fixture.sh" + +FIXTURE_SCENARIO=registered +export FIXTURE_SCENARIO +[ "$(at /dev/null "$command")" = $'REGISTERING\r' ] +[ "$(at /dev/null "$command")" = $'REGISTERED\r' ] +at /dev/null "$command" >/dev/null 2>&1 +[ "$?" -eq 127 ] + +rm -f "$FIXTURE_LOOKUP/cursors/$hash" +FIXTURE_SCENARIO=sim-absent +export FIXTURE_SCENARIO +output=$(at /dev/null "$command") +rc=$? +[ "$output" = $'NO SIM\r' ] +[ "$rc" -eq 3 ] + +echo 'fixture response queue tests passed' diff --git a/application/qmodem/tests/test_import_testcases.sh b/application/qmodem/tests/test_import_testcases.sh new file mode 100755 index 00000000..274f669c --- /dev/null +++ b/application/qmodem/tests/test_import_testcases.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Legacy single-response feedback is normalized to responses[] during import. +set -euo pipefail + +TESTS_DIR=$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(CDPATH= cd "$TESTS_DIR/../../.." && pwd) +test_root=$(mktemp -d) +archive=$(mktemp --suffix=.tar.gz) +source_dir="$test_root/source/quectel/qualcomm/example-1a79a4d6" +cleanup() { rm -rf "$test_root" "$archive"; } +trap cleanup EXIT + +mkdir -p "$source_dir" +jq -n '{vendor:"quectel",platform:"qualcomm",model:"example",command:"AT+CGSN", + tool:"at",response_hex:"4f4b0d0a",rc:0,timestamp:"legacy"}' \ + > "$source_dir/AT_CGSN.json" +mkdir -p "$source_dir/expected" +jq -n '{result:{},imei:"legacy"}' > "$source_dir/expected/get_imei.json" +tar -czf "$archive" -C "$test_root/source" . + +QMODEM_TEST_REPO_ROOT="$test_root/import" "$REPO_ROOT/scripts/import_testcases.sh" "$archive" >/dev/null +fixture="$test_root/import/testcases/quectel/qualcomm/example-1a79a4d6/AT_CGSN.json" +jq -e ' + (.responses | length) == 1 and + .responses[0] == {response_hex:"4f4b0d0a",rc:0,timestamp:"legacy",scenario:"default",sequence:0,capture_sequence:0} and + (has("response_hex") | not) and (has("rc") | not)' "$fixture" >/dev/null +[ -f "$test_root/import/testcases/quectel/qualcomm/example-1a79a4d6/expected/default/get_imei.json" ] + +# Importing another capture merges stateful responses and remains idempotent. +jq -n '{vendor:"quectel",platform:"qualcomm",model:"example",command:"AT+CGSN",tool:"at", + responses:[ + {response_hex:"4552524f520d0a",rc:1,timestamp:"second",scenario:"default",sequence:0}, + {response_hex:"524f414d494e470d0a",rc:0,timestamp:"second",scenario:"roaming",sequence:0} + ]}' > "$source_dir/AT_CGSN.json" +tar -czf "$archive" -C "$test_root/source" . +QMODEM_TEST_REPO_ROOT="$test_root/import" "$REPO_ROOT/scripts/import_testcases.sh" "$archive" >/dev/null +QMODEM_TEST_REPO_ROOT="$test_root/import" "$REPO_ROOT/scripts/import_testcases.sh" "$archive" >/dev/null +jq -e ' + (.responses | length) == 3 and + ([.responses[] | select(.scenario == "default") | .sequence] == [0,1]) and + ([.responses[] | select(.scenario == "roaming") | .sequence] == [0]) +' "$fixture" >/dev/null + +echo 'testcase import compatibility tests passed' diff --git a/application/qmodem/tests/test_recognition_fixtures.sh b/application/qmodem/tests/test_recognition_fixtures.sh index 8616c066..bf188514 100755 --- a/application/qmodem/tests/test_recognition_fixtures.sh +++ b/application/qmodem/tests/test_recognition_fixtures.sh @@ -54,9 +54,16 @@ for fixture in "${fixture_files[@]}"; do fi command=$(jq -r '.command // empty' "$fixture") - response_hex=$(jq -r '.response_hex // empty' "$fixture") - if [ -z "$command" ] || ! printf '%s' "$response_hex" | grep -Eq '^([0-9a-fA-F]{2})*$'; then - echo "FAIL: $label: invalid command or response_hex" + if [ -z "$command" ] || ! jq -e ' + (.responses | type) == "array" and (.responses | length) > 0 and + all(.responses[]; + ((.rc // 0) | type) == "number" and + ((.scenario | type) == "string" and (.scenario | test("^[a-z0-9._-]+$"))) and + ((.sequence | type) == "number" and .sequence >= 0) and + ((.capture_sequence | type) == "number" and .capture_sequence >= 0) and + ((.response_hex | type) == "string" and (.response_hex | test("^([0-9a-fA-F]{2})*$")))) + ' "$fixture" >/dev/null; then + echo "FAIL: $label: invalid command or responses" fail=1 continue fi diff --git a/application/qmodem/tests/test_vendor_fixtures.sh b/application/qmodem/tests/test_vendor_fixtures.sh index bbcbb575..9d9c7908 100755 --- a/application/qmodem/tests/test_vendor_fixtures.sh +++ b/application/qmodem/tests/test_vendor_fixtures.sh @@ -77,12 +77,27 @@ for profile_dir in "${profile_dirs[@]}"; do LOOKUP=$(mktemp -d) CMDS_LIST="$LOOKUP/commands.txt" + SCENARIOS="$LOOKUP/scenarios.txt" : > "$CMDS_LIST" + : > "$SCENARIOS" for f in "$profile_dir"/*.json; do [ -f "$f" ] || continue fixture_vendor=$(jq -r '.vendor // empty' "$f") fixture_platform=$(jq -r '.platform // empty' "$f") current_model=$(jq -r '.model // empty' "$f") + if ! jq -e ' + (.responses | type) == "array" and (.responses | length) > 0 and + all(.responses[]; + ((.scenario | type) == "string" and (.scenario | test("^[a-z0-9._-]+$"))) and + ((.sequence | type) == "number" and .sequence >= 0) and + ((.capture_sequence | type) == "number" and .capture_sequence >= 0) and + ((.rc // 0) | type) == "number" and + (((.response_hex | type) == "string" and (.response_hex | test("^([0-9a-fA-F]{2})*$"))) or + (.response != null)))' "$f" >/dev/null; then + echo "FAIL: $profile_label: invalid responses in $(basename "$f")" + fail=1 + continue + fi if [ "$fixture_vendor" != "$profile_vendor_value" ] || \ [ "$fixture_platform" != "$profile_platform_value" ] || \ [ "$current_model" != "$fixture_model" ]; then @@ -92,18 +107,30 @@ for profile_dir in "${profile_dirs[@]}"; do fi cmd=$(jq -r '.command' "$f") h=$(printf '%s' "$cmd" | md5sum | cut -c1-8) - if [ -e "$LOOKUP/$h.response" ]; then + if [ -d "$LOOKUP/$h.responses" ]; then echo "FAIL: $profile_label: duplicate command fixture: $cmd" fail=1 continue fi - response_hex=$(jq -r '.response_hex // empty' "$f") - if [ -n "$response_hex" ]; then - fixture_hex_decode "$response_hex" > "$LOOKUP/$h.response" - else - jq -j '.response' "$f" > "$LOOKUP/$h.response" - fi - jq -r '.rc // 0' "$f" > "$LOOKUP/$h.rc" + mkdir -p "$LOOKUP/$h.responses" + while IFS= read -r response; do + scenario=$(jq -r '.scenario // "default"' <<< "$response") + if [ "$(profile_segment "$scenario" default)" != "$scenario" ]; then + echo "FAIL: $profile_label: invalid response scenario '$scenario' in $(basename "$f")" + fail=1 + continue + fi + mkdir -p "$LOOKUP/$h.responses/$scenario" + response_index=$(find "$LOOKUP/$h.responses/$scenario" -name '*.response' | wc -l) + response_hex=$(jq -r '.response_hex // empty' <<< "$response") + if [ -n "$response_hex" ]; then + fixture_hex_decode "$response_hex" > "$LOOKUP/$h.responses/$scenario/$response_index.response" + else + jq -j '.response' <<< "$response" > "$LOOKUP/$h.responses/$scenario/$response_index.response" + fi + jq -r '.rc // 0' <<< "$response" > "$LOOKUP/$h.responses/$scenario/$response_index.rc" + printf '%s\n' "$scenario" >> "$SCENARIOS" + done < <(jq -c '.responses | sort_by(.scenario // "default", .sequence // 0)[]' "$f") printf '%s\n' "$cmd" >> "$CMDS_LIST" done @@ -118,84 +145,99 @@ for profile_dir in "${profile_dirs[@]}"; do fi done < "$CMDS_LIST" - ( - . "$TESTS_DIR/lib/test_env.sh" - vendor="$vendor" - platform="$profile_platform" - QMODEM_TESTCASE_MODEL="$fixture_model" - QMODEM_TESTCASE_MODES="$fixture_modes" - export vendor platform QMODEM_TESTCASE_MODEL QMODEM_TESTCASE_MODES - . "$QMODEM_JSHN" - . "$vendor_file" - . "$cmds_file" - FIXTURE_LOOKUP="$LOOKUP" - export FIXTURE_LOOKUP - . "$TESTS_DIR/lib/at_fixture.sh" - - for fixture in "$profile_dir"/*.json; do - [ -f "$fixture" ] || continue - fixture_cmd=$(jq -r '.command' "$fixture") - fixture_tool=$(jq -r '.tool // "at"' "$fixture") - fixture_rc=$(jq -r '.rc // 0' "$fixture") - expected_file="$LOOKUP/expected.response" - replayed_file="$LOOKUP/replayed.response" - fixture_hex=$(jq -r '.response_hex // empty' "$fixture") - if [ -n "$fixture_hex" ]; then - fixture_hex_decode "$fixture_hex" > "$expected_file" - else - jq -j '.response' "$fixture" > "$expected_file" - fi - "$fixture_tool" "$at_port" "$fixture_cmd" > "$replayed_file" - replayed_rc=$? - if [ "$replayed_rc" -ne "$fixture_rc" ] || ! cmp "$expected_file" "$replayed_file"; then - echo "FAIL: $profile_label: fixture replay mismatch: $(basename "$fixture")" - exit 1 - fi - done + while IFS= read -r fixture_scenario; do + [ -n "$fixture_scenario" ] || continue + ( + . "$TESTS_DIR/lib/test_env.sh" + vendor="$vendor" + platform="$profile_platform" + QMODEM_TESTCASE_MODEL="$fixture_model" + QMODEM_TESTCASE_MODES="$fixture_modes" + FIXTURE_LOOKUP="$LOOKUP" + FIXTURE_SCENARIO="$fixture_scenario" + export vendor platform QMODEM_TESTCASE_MODEL QMODEM_TESTCASE_MODES + export FIXTURE_LOOKUP FIXTURE_SCENARIO + . "$QMODEM_JSHN" + . "$vendor_file" + . "$cmds_file" + mkdir -p "$FIXTURE_LOOKUP/cursors" + . "$TESTS_DIR/lib/at_fixture.sh" - for exp in "$profile_dir"/expected/*.json; do - [ -f "$exp" ] || continue - method=$(basename "$exp" .json) - case "$method" in - base_info|cell_info|get_*) ;; - *) echo "WARN: $profile_label: refusing non-read-only snapshot method: $method"; continue ;; - esac - if ! command -v "$method" >/dev/null; then - echo "WARN: $profile_label: $method not defined, skipping snapshot" - continue - fi - json_init - json_add_object result - json_close_object - case "$method" in - base_info|cell_info) - json_add_array modem_info - "$method" - json_close_array - ;; - *) "$method" ;; - esac - out=$(json_dump) - rc=$? - if [ "$rc" -ne 0 ] || ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then - echo "FAIL: $profile_label: $method failed (rc=$rc) or emitted invalid JSON" - exit 1 - fi - if ! diff <(jq -S . "$exp") <(printf '%s' "$out" | jq -S .) >/dev/null; then - echo "FAIL: $profile_label: $method differs from expected/$method.json:" - diff <(jq -S . "$exp") <(printf '%s' "$out" | jq -S .) | head -20 - exit 1 - fi - echo "OK: $profile_label: $method matches golden snapshot" - done + # First prove the queue preserves every recorded byte sequence and rc. + rm -f "$FIXTURE_LOOKUP/cursors"/* + for fixture in "$profile_dir"/*.json; do + [ -f "$fixture" ] || continue + fixture_cmd=$(jq -r '.command' "$fixture") + fixture_tool=$(jq -r '.tool // "at"' "$fixture") + while IFS= read -r response; do + fixture_rc=$(jq -r '.rc // 0' <<< "$response") + fixture_hex=$(jq -r '.response_hex // empty' <<< "$response") + expected_file="$LOOKUP/expected.response" + replayed_file="$LOOKUP/replayed.response" + if [ -n "$fixture_hex" ]; then + fixture_hex_decode "$fixture_hex" > "$expected_file" + else + jq -j '.response' <<< "$response" > "$expected_file" + fi + "$fixture_tool" "$at_port" "$fixture_cmd" > "$replayed_file" + replayed_rc=$? + if [ "$replayed_rc" -ne "$fixture_rc" ] || ! cmp "$expected_file" "$replayed_file"; then + echo "FAIL: $profile_label/$fixture_scenario: queue replay mismatch: $(basename "$fixture")" + exit 1 + fi + done < <(jq -c --arg scenario "$fixture_scenario" \ + '.responses | map(select((.scenario // "default") == $scenario)) | sort_by(.sequence // 0)[]' "$fixture") + done + + expected_dir="$profile_dir/expected/$fixture_scenario" + for exp in "$expected_dir"/*.json; do + [ -f "$exp" ] || continue + method=$(basename "$exp" .json) + case "$method" in + base_info|cell_info|get_*) ;; + *) echo "WARN: $profile_label/$fixture_scenario: refusing non-read-only snapshot: $method"; continue ;; + esac + if ! command -v "$method" >/dev/null; then + echo "WARN: $profile_label/$fixture_scenario: $method not defined, skipping snapshot" + continue + fi + rm -f "$FIXTURE_LOOKUP/cursors"/* "$FIXTURE_LOOKUP/misses.log" + out=$( + json_init + json_add_object result + json_close_object + case "$method" in + base_info|cell_info) + json_add_array modem_info + "$method" + json_close_array + ;; + *) "$method" ;; + esac + json_dump + ) + rc=$? + if [ -s "$FIXTURE_LOOKUP/misses.log" ]; then + echo "FAIL: $profile_label/$fixture_scenario: $method exhausted or missed fixture responses" + sed 's/^/ /' "$FIXTURE_LOOKUP/misses.log" + exit 1 + fi + if [ "$rc" -ne 0 ] || ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then + echo "FAIL: $profile_label/$fixture_scenario: $method failed (rc=$rc) or emitted invalid JSON" + exit 1 + fi + if ! diff <(jq -S . "$exp") <(printf '%s' "$out" | jq -S .) >/dev/null; then + echo "FAIL: $profile_label/$fixture_scenario: $method differs from expected snapshot:" + diff <(jq -S . "$exp") <(printf '%s' "$out" | jq -S .) | head -20 + exit 1 + fi + echo "OK: $profile_label/$fixture_scenario: $method matches golden snapshot" + done - echo "OK: $profile_label: replay passed" - ) || fail=1 + echo "OK: $profile_label/$fixture_scenario: replay passed" + ) || fail=1 + done < <(sort -u "$SCENARIOS") - if [ -f "$LOOKUP/misses.log" ]; then - echo "INFO: $profile_label: commands used but without fixtures:" - sort -u "$LOOKUP/misses.log" | sed 's/^/ /' | head -10 - fi rm -rf "$LOOKUP" done diff --git a/scripts/import_testcases.sh b/scripts/import_testcases.sh index a7de5cd4..15e7da1c 100755 --- a/scripts/import_testcases.sh +++ b/scripts/import_testcases.sh @@ -5,17 +5,46 @@ set -eu [ $# -ge 1 ] || { echo "usage: $0 " >&2; exit 1; } tarball="$1" [ -f "$tarball" ] || { echo "not found: $tarball" >&2; exit 1; } -repo_root=$(CDPATH= cd "$(dirname "$0")/.." && pwd) +repo_root=${QMODEM_TEST_REPO_ROOT:-$(CDPATH= cd "$(dirname "$0")/.." && pwd)} tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT tar -xzf "$tarball" -C "$tmp" +# Legacy snapshots had no state boundary. Place them in the default scenario. +find "$tmp" -type f -name '*.json' -path '*/expected/*.json' | while IFS= read -r f; do + [ "$(basename "$(dirname "$f")")" = expected ] || continue + mkdir -p "$(dirname "$f")/default" + mv "$f" "$(dirname "$f")/default/$(basename "$f")" +done + # validate every fixture before merging; archives may contain fixtures only fixture_list="$tmp/fixture-list" find "$tmp" -name '*.json' -type f > "$fixture_list" [ -s "$fixture_list" ] || { echo "archive contains no fixtures" >&2; exit 1; } while IFS= read -r f; do + case "$f" in + */expected/*) ;; + *) + if ! jq -e 'has("responses")' "$f" >/dev/null 2>&1; then + normalized="${f}.normalized" + jq '.responses=[{response_hex:(.response_hex // ""), response:.response, + rc:(.rc // 0), timestamp:(.timestamp // ""), + scenario:"default", sequence:0, capture_sequence:0}] + | .responses |= map(if .response == null then del(.response) else . end) + | del(.response_hex, .response, .rc, .timestamp)' "$f" > "$normalized" || { + echo "invalid legacy fixture: $f" >&2 + exit 1 + } + mv "$normalized" "$f" + fi + normalized="${f}.normalized" + jq '.responses |= to_entries | .responses |= map( + .value + {scenario:(.value.scenario // "default"), sequence:(.value.sequence // .key), + capture_sequence:(.value.capture_sequence // .value.sequence // .key)})' \ + "$f" > "$normalized" && mv "$normalized" "$f" + ;; + esac case "$f" in "$tmp"/recognition/pending/*) echo "unresolved recognition fixture: $f" >&2 @@ -24,11 +53,25 @@ while IFS= read -r f; do "$tmp"/recognition/*/*/*/*.json) filter='.phase == "recognition" and .config_section and .command and (.expected_identity.vendor and .expected_identity.platform and .expected_identity.model) and - (((.response_hex | type) == "string" and (.response_hex | test("^([0-9a-fA-F]{2})*$"))) or (.response != null))' + ((.responses | type) == "array" and (.responses | length) > 0) and + all(.responses[]; + ((.scenario | type) == "string" and (.scenario | test("^[a-z0-9._-]+$"))) and + ((.sequence | type) == "number" and .sequence >= 0) and + ((.capture_sequence | type) == "number" and .capture_sequence >= 0) and + ((.rc // 0) | type) == "number" and + (((.response_hex | type) == "string" and (.response_hex | test("^([0-9a-fA-F]{2})*$"))) or + (.response != null)))' ;; - "$tmp"/*/*/*/expected/*.json) filter='type == "object"' ;; + "$tmp"/*/*/*/expected/*/*.json) filter='type == "object"' ;; "$tmp"/*/*/*/*.json) filter='.vendor and .platform and .model and .command and - (((.response_hex | type) == "string" and (.response_hex | test("^([0-9a-fA-F]{2})*$"))) or (.response != null))' ;; + ((.responses | type) == "array" and (.responses | length) > 0) and + all(.responses[]; + ((.scenario | type) == "string" and (.scenario | test("^[a-z0-9._-]+$"))) and + ((.sequence | type) == "number" and .sequence >= 0) and + ((.capture_sequence | type) == "number" and .capture_sequence >= 0) and + ((.rc // 0) | type) == "number" and + (((.response_hex | type) == "string" and (.response_hex | test("^([0-9a-fA-F]{2})*$"))) or + (.response != null)))' ;; *) echo "invalid fixture path (expected vendor/platform/model): $f" >&2 exit 1 @@ -40,14 +83,26 @@ while IFS= read -r f; do } done < "$fixture_list" -# merge (same relative path overwrites; filename = command hash, so reruns dedupe) +# Merge command responses by scenario. Expected snapshots remain scenario-specific files. cd "$tmp" find . -name '*.json' -type f > "$fixture_list" while IFS= read -r f; do rel=${f#./} dst="$repo_root/testcases/$rel" mkdir -p "$(dirname "$dst")" - cp "$f" "$dst" + if [ -f "$dst" ] && [ "${rel#*/expected/}" = "$rel" ]; then + merged="${dst}.merged" + jq -s ' + .[0] as $old | .[1] as $new | ($old * $new) + | .responses = ([$old.responses[], $new.responses[]] + | unique_by([.scenario, .capture_sequence, .timestamp, .response_hex, .response, .rc]) + | group_by(.scenario) + | map(sort_by(.sequence) | to_entries | map(.value + {sequence:.key})) + | add) + ' "$dst" "$f" > "$merged" && mv "$merged" "$dst" + else + cp "$f" "$dst" + fi done < "$fixture_list" cd "$repo_root" diff --git a/testcases/README.md b/testcases/README.md index 325c9207..1814c852 100644 --- a/testcases/README.md +++ b/testcases/README.md @@ -14,7 +14,8 @@ testcases/ -/ # 同名、空格和特殊字符不会造成目录冲突 AT_CGSN-7c58b773.json # 一条指令的采集记录 expected/ - get_imei.json # 该型号/平台专属的黄金输出 + registered/ + get_imei.json # 该型号、场景专属的黄金输出 ``` ## fixture 格式 @@ -25,18 +26,26 @@ testcases/ "platform": "qualcomm", "model": "RM500Q-AE", "command": "AT+CGSN", - "response_hex": "41542b4347534e0d0d0a3836303030303030303030303031320d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-07T00:00:00Z", + "responses": [ + { + "response_hex": "41542b4347534e0d0a3836303030303030303030303031320d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-07T00:00:00Z", + "scenario": "registered", + "sequence": 0, + "capture_sequence": 0 + } + ], "sanitized": true } ``` - `command`:实际发送的完整 AT 指令(含参数)。 - `vendor`、`platform`、`model`:采集时的厂商、平台和 UCI `name`(Modem Model)。回放以三者组成的设备画像为边界;同一条 AT 指令可以在不同画像中保存不同响应。 +- `responses`:同一命令在不同设备或状态下采集的返回队列。`scenario` 标识一组可同时成立的设备状态,`sequence` 表示该场景内同一命令的回放顺序;`capture_sequence` 是采集时的稳定编号,供重复导入去重。即使返回字节相同,重复调用也会保留。 - `response_hex`:模组原始 stdout 的十六进制编码,由 `xxd -p` 生成;可无损保存 CR/LF、尾部换行及任意二进制字节。 -- `tool`:`at` 或 `fastat`;`rc`:发送工具的退出码。 +- `tool`:`at` 或 `fastat`;每个 response 的 `rc` 是发送工具退出码。 - `sanitized`:`qmodem_collect pack` 默认脱敏(≥11 位数字串保留头2尾2、中间置 0,长度不变),标记为 true;`pack --raw` 可关闭脱敏(注意隐私)。 - `capabilities.modes`:仅用于不在 `modem_support.json` 中的合成 fixture;真实型号的拨号模式由 runner 从能力表读取。 @@ -52,6 +61,7 @@ testcases/ ```sh uci set qmodem.main.testcase_collect=1 && uci commit qmodem +uci set qmodem.main.testcase_scenario=registered && uci commit qmodem # 通过 LuCI / ubus / CLI 触发各功能(base_info、cell_info、锁频、锁小区……) qmodem_collect status # 查看已采集数量 qmodem_collect pack # 默认脱敏并加密到 /tmp/qmodem_feedback_<时间戳>.tar @@ -101,8 +111,8 @@ bash application/qmodem/tests/test_recognition_fixtures.sh 三层校验: 1. fixture 的路径必须与 `vendor/platform/model` 元数据一致,且指令头仍存在于对应 `cmds/.sh`; -2. 每个设备画像建立独立命令响应表,用 fixture 回放 `at`/`fastat`;不同型号或平台不会互相覆盖; -3. vendor 只读方法必须退出码 0 且输出合法 JSON; -4. 存在画像专属 `expected/.json` 时,方法输出经 `jq -S` 归一化后与快照精确比对。 +2. 每个设备画像按 scenario 建立独立命令队列,用 fixture 顺序回放 `at`/`fastat`;不同状态不会被拼成一个不存在的混合状态; +3. 场景内命令响应缺失或队列耗尽会直接失败,不会被当作空响应成功; +4. 存在 `expected//.json` 时,方法输出经 `jq -S` 归一化后与该场景快照精确比对。 末尾会打印 cmds 指令的 fixture 覆盖报告(仅提示,不失败——覆盖率依赖真机捐赠)。 diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_BAND_-880751ee.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_BAND_-880751ee.json index 59c2aeb0..4ee1dcfc 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_BAND_-880751ee.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_BAND_-880751ee.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT*BAND?", - "response_hex": "41542a42414e443f0d0a0d0a2b434d45204552524f523a20696e76616c696420636f6d6d616e64206c696e650d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:56:14Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542a42414e443f0d0a0d0a2b434d45204552524f523a20696e76616c696420636f6d6d616e64206c696e650d0a", + "rc": 0, + "timestamp": "2026-08-08T10:56:14Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CBC-128cd781.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CBC-128cd781.json index 9f263c48..121cf881 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CBC-128cd781.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CBC-128cd781.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CBC", - "response_hex": "41542b4342430d0a0d0a2b4342433a20312c38342c343031340d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:24Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b4342430d0a0d0a2b4342433a20312c38342c343031340d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:24Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CCED_0_1-8d495ebe.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CCED_0_1-8d495ebe.json index e2c16b08..45aa9129 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CCED_0_1-8d495ebe.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CCED_0_1-8d495ebe.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CCED=0,1", - "response_hex": "41542b434345443d302c310d0a0d0a2b434345443a4c54452063757272656e742063656c6c3a3436302c31312c3436303030303030303030303039302c302c332c6e3130302c313635302c3131313734323838312c35312c31392c363334322c33382c38310d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:16Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b434345443d302c310d0a0d0a2b434345443a4c54452063757272656e742063656c6c3a3436302c31312c3436303030303030303030303039302c302c332c6e3130302c313635302c3131313734323838312c35312c31392c363334322c33382c38310d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:16Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CESQ-61420008.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CESQ-61420008.json index ce546cb6..4e1d22b7 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CESQ-61420008.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CESQ-61420008.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CESQ", - "response_hex": "41542b434553510d0a0d0a2b434553513a2039392c39392c3235352c3235352c31392c35310d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:17Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b434553510d0a0d0a2b434553513a2039392c39392c3235352c3235352c31392c35310d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:17Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGACT_-a446511b.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGACT_-a446511b.json index 5df99a23..1ccdd277 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGACT_-a446511b.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGACT_-a446511b.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CGACT?", - "response_hex": "41542b43474143543f0d0a0d0a2b43474143543a20352c20310d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:24Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b43474143543f0d0a0d0a2b43474143543a20352c20310d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:24Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMI-89cca6a4.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMI-89cca6a4.json index e8b5a606..3330973b 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMI-89cca6a4.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMI-89cca6a4.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CGMI", - "response_hex": "41542b43474d490d0a0d0a2b43474d493a2022220d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:24Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b43474d490d0a0d0a2b43474d493a2022220d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:24Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json index fc262da9..34028e0e 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CGMM", - "response_hex": "41542b43474d4d0d0a0d0a2b43474d4d3a20224169723732345547220d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:24Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b43474d4d0d0a0d0a2b43474d4d3a20224169723732345547220d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:24Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMR-4b35c614.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMR-4b35c614.json index b647a970..0c5c81d6 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMR-4b35c614.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGMR-4b35c614.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CGMR", - "response_hex": "41542b43474d520d0a0d0a2b43474d523a20224c7561744f532d4169725f56343033325f524441383931305f5454535f4e4f4c56474c5f464c4f4154220d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:24Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b43474d520d0a0d0a2b43474d523a20224c7561744f532d4169725f56343033325f524441383931305f5454535f4e4f4c56474c5f464c4f4154220d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:24Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGPADDR_5-adeff2de.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGPADDR_5-adeff2de.json index e4f666c9..a3fdeaa1 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGPADDR_5-adeff2de.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGPADDR_5-adeff2de.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CGPADDR=5", - "response_hex": "41542b434750414444523d350d0a0d0a2b434750414444523a20352c22495056343a31302e33322e3134302e31323220202020495056363a3a3a313843393a434533333a393742373a37354244220d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:24Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b434750414444523d350d0a0d0a2b434750414444523a20352c22495056343a31302e33322e3134302e31323220202020495056363a3a3a313843393a434533333a393742373a37354244220d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:24Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGSN-7c58b773.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGSN-7c58b773.json index 986da4f5..993e63e3 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CGSN-7c58b773.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CGSN-7c58b773.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CGSN", - "response_hex": "41542b4347534e0d0a0d0a3836303030303030303030303039320d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:15Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b4347534e0d0a0d0a3836303030303030303030303039320d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:15Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CIMI-a35d2cb4.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CIMI-a35d2cb4.json index 4bb33ee0..d43a855a 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CIMI-a35d2cb4.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CIMI-a35d2cb4.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CIMI", - "response_hex": "41542b43494d490d0a0d0a3436303030303030303030303039300d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:16Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b43494d490d0a0d0a3436303030303030303030303039300d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:16Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CNUM-1ac209a6.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CNUM-1ac209a6.json index 8119dcd9..78296b72 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CNUM-1ac209a6.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CNUM-1ac209a6.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CNUM", - "response_hex": "41542b434e554d0d0a0d0a2b434e554d3a2022222c222b38363030303030303030303737222c3134350d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:16Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b434e554d0d0a0d0a2b434e554d3a2022222c222b38363030303030303030303737222c3134350d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:16Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_COPS_-99f371b4.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_COPS_-99f371b4.json index 4355cfb5..b09032b1 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_COPS_-99f371b4.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_COPS_-99f371b4.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+COPS?", - "response_hex": "41542b434f50533f0d0a0d0a2b434f50533a20302c322c223436303131222c370d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:16Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b434f50533f0d0a0d0a2b434f50533a20302c322c223436303131222c370d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:16Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CPIN_-cc2cd68a.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CPIN_-cc2cd68a.json index e280c316..f660a0a1 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CPIN_-cc2cd68a.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CPIN_-cc2cd68a.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CPIN?", - "response_hex": "41542b4350494e3f0d0a0d0a2b4350494e3a2052454144590d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:15Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b4350494e3f0d0a0d0a2b4350494e3a2052454144590d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:15Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CPMS_-8a28a28e.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CPMS_-8a28a28e.json index 73177aca..a2c7e684 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CPMS_-8a28a28e.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CPMS_-8a28a28e.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CPMS?", - "response_hex": "41542b43504d533f0d0a0d0a2b43504d533a2022534d222c302c34302c22534d222c302c34302c22534d222c302c34300d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:56:58Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b43504d533f0d0a0d0a2b43504d533a2022534d222c302c34302c22534d222c302c34302c22534d222c302c34300d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:56:58Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CSQ-fecb12e1.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CSQ-fecb12e1.json index d1e8fc04..a601458e 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CSQ-fecb12e1.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CSQ-fecb12e1.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CSQ", - "response_hex": "41542b4353510d0a0d0a2b4353513a2032372c39390d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:16Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b4353510d0a0d0a2b4353513a2032372c39390d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:16Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_CTEC_-117d6151.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_CTEC_-117d6151.json index bc2f5b83..935eed44 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_CTEC_-117d6151.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_CTEC_-117d6151.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+CTEC?", - "response_hex": "41542b435445433f0d0a0d0a2b435445433a20302c300d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:56:11Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b435445433f0d0a0d0a2b435445433a20302c300d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:56:11Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMGINFO_-a1208af7.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMGINFO_-a1208af7.json index 33861e1f..40ecdbfa 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMGINFO_-a1208af7.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMGINFO_-a1208af7.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+EEMGINFO?", - "response_hex": "41542b45454d47494e464f3f0d0a0d0a2b45454d47494e464f3a20332c20320d0a0d0a4f4b0d0a0d0a2b45454d4c54455356433a20313132302c20322c2031372c20363334322c2038312c20313635302c20313635302c20332c20352c203131313734323838312c2036353533352c2035312c2032312c20362c2035312c2032312c2035312c2032312c2036353533352c20302c203235352c20302c20302c20302c20302c20302c2036353533352c20302c20302c20300d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:17Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b45454d47494e464f3f0d0a0d0a2b45454d47494e464f3a20332c20320d0a0d0a4f4b0d0a0d0a2b45454d4c54455356433a20313132302c20322c2031372c20363334322c2038312c20313635302c20313635302c20332c20352c203131313734323838312c2036353533352c2035312c2032312c20362c2035312c2032312c2035312c2032312c2036353533352c20302c203235352c20302c20302c20302c20302c20302c2036353533352c20302c20302c20300d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:17Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMOPT_1-1e854f5d.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMOPT_1-1e854f5d.json index 23c01566..eb4d3d53 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMOPT_1-1e854f5d.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_EEMOPT_1-1e854f5d.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+EEMOPT=1", - "response_hex": "41542b45454d4f50543d310d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:17Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b45454d4f50543d310d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:17Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_ICCID-ba93350e.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_ICCID-ba93350e.json index 7d93366e..ba410c74 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_ICCID-ba93350e.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_ICCID-ba93350e.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+ICCID", - "response_hex": "41542b49434349440d0a0d0a2b49434349443a2038393030303030303030303030303030303032320d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:16Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b49434349440d0a0d0a2b49434349443a2038393030303030303030303030303030303032320d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:16Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_RNDISCALL_0_0-b24cd777.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_RNDISCALL_0_0-b24cd777.json index 6912fd0a..bec05d08 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_RNDISCALL_0_0-b24cd777.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_RNDISCALL_0_0-b24cd777.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+RNDISCALL=0,0", - "response_hex": "41542b524e44495343414c4c3d302c300d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:53Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b524e44495343414c4c3d302c300d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:53Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_-b51511f9.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_-b51511f9.json index dafe8ec8..c2520ec8 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_-b51511f9.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_-b51511f9.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+SETUSB?", - "response_hex": "41542b5345545553423f0d0a0d0a6d6f64653a20320d0a5669643a203078313738320d0a5069643a203078346530300d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:56:10Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b5345545553423f0d0a0d0a6d6f64653a20320d0a5669643a203078313738320d0a5069643a203078346530300d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:56:10Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_1-af4420f4.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_1-af4420f4.json index b0bcf1c1..77e9d5ca 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_1-af4420f4.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_1-af4420f4.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+SETUSB=1", - "response_hex": "41542b5345545553423d310d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:35Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b5345545553423d310d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:35Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_2-669c241f.json b/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_2-669c241f.json index ea824165..f942375d 100644 --- a/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_2-669c241f.json +++ b/testcases/openluat/unisoc/air724ug-c9340434/AT_SETUSB_2-669c241f.json @@ -3,9 +3,16 @@ "platform": "unisoc", "model": "air724ug", "command": "AT+SETUSB=2", - "response_hex": "41542b5345545553423d320d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:55:53Z", - "sanitized": true + "sanitized": true, + "responses": [ + { + "response_hex": "41542b5345545553423d320d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:55:53Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/base_info.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/base_info.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/base_info.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/base_info.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/delete_sms.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/delete_sms.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/delete_sms.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/delete_sms.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/get_disabled_features.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_disabled_features.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/get_disabled_features.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_disabled_features.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/get_lockband.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_lockband.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/get_lockband.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_lockband.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/get_mode.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_mode.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/get_mode.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_mode.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/get_network_prefer.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_network_prefer.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/get_network_prefer.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/get_network_prefer.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/info.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/info.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/info.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/info.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/send_raw_pdu.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/send_raw_pdu.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/send_raw_pdu.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/send_raw_pdu.json diff --git a/testcases/openluat/unisoc/air724ug-c9340434/expected/set_mode.json b/testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/set_mode.json similarity index 100% rename from testcases/openluat/unisoc/air724ug-c9340434/expected/set_mode.json rename to testcases/openluat/unisoc/air724ug-c9340434/expected/feedback-20260808/set_mode.json diff --git a/testcases/quectel/qualcomm/example-1a79a4d6/AT_CGSN-7c58b773.json b/testcases/quectel/qualcomm/example-1a79a4d6/AT_CGSN-7c58b773.json index a59e96db..70aece70 100644 --- a/testcases/quectel/qualcomm/example-1a79a4d6/AT_CGSN-7c58b773.json +++ b/testcases/quectel/qualcomm/example-1a79a4d6/AT_CGSN-7c58b773.json @@ -2,12 +2,27 @@ "vendor": "quectel", "platform": "qualcomm", "model": "example", - "capabilities": { "modes": ["qmi", "ecm", "mbim", "rndis", "ncm"] }, + "capabilities": { + "modes": [ + "qmi", + "ecm", + "mbim", + "rndis", + "ncm" + ] + }, "command": "AT+CGSN", - "response_hex": "41542b4347534e0d0d0a3836303030303030303030303031320d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-07T00:00:00Z", "sanitized": true, - "source": "example" + "source": "example", + "responses": [ + { + "response_hex": "41542b4347534e0d0d0a3836303030303030303030303031320d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-07T00:00:00Z", + "scenario": "default", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/quectel/qualcomm/example-1a79a4d6/AT_QCFG__usbnet_-f2c91ace.json b/testcases/quectel/qualcomm/example-1a79a4d6/AT_QCFG__usbnet_-f2c91ace.json index 8a75d26d..83d15b2e 100644 --- a/testcases/quectel/qualcomm/example-1a79a4d6/AT_QCFG__usbnet_-f2c91ace.json +++ b/testcases/quectel/qualcomm/example-1a79a4d6/AT_QCFG__usbnet_-f2c91ace.json @@ -2,12 +2,27 @@ "vendor": "quectel", "platform": "qualcomm", "model": "example", - "capabilities": { "modes": ["qmi", "ecm", "mbim", "rndis", "ncm"] }, + "capabilities": { + "modes": [ + "qmi", + "ecm", + "mbim", + "rndis", + "ncm" + ] + }, "command": "AT+QCFG=\"usbnet\"", - "response_hex": "41542b514346473d227573626e6574220d0d0a2b514346473a20227573626e6574222c300d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-07T00:00:00Z", "sanitized": true, - "source": "example" + "source": "example", + "responses": [ + { + "response_hex": "41542b514346473d227573626e6574220d0d0a2b514346473a20227573626e6574222c300d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-07T00:00:00Z", + "scenario": "default", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/quectel/qualcomm/example-1a79a4d6/expected/get_imei.json b/testcases/quectel/qualcomm/example-1a79a4d6/expected/default/get_imei.json similarity index 100% rename from testcases/quectel/qualcomm/example-1a79a4d6/expected/get_imei.json rename to testcases/quectel/qualcomm/example-1a79a4d6/expected/default/get_imei.json diff --git a/testcases/quectel/qualcomm/example-1a79a4d6/expected/get_mode.json b/testcases/quectel/qualcomm/example-1a79a4d6/expected/default/get_mode.json similarity index 100% rename from testcases/quectel/qualcomm/example-1a79a4d6/expected/get_mode.json rename to testcases/quectel/qualcomm/example-1a79a4d6/expected/default/get_mode.json diff --git a/testcases/recognition/openluat/unisoc/air724ug-c9340434/ATI-9226517c.json b/testcases/recognition/openluat/unisoc/air724ug-c9340434/ATI-9226517c.json index bf369a8e..b8425936 100644 --- a/testcases/recognition/openluat/unisoc/air724ug-c9340434/ATI-9226517c.json +++ b/testcases/recognition/openluat/unisoc/air724ug-c9340434/ATI-9226517c.json @@ -3,10 +3,7 @@ "platform": "unknown", "model": "unknown", "command": "ATI", - "response_hex": "4154490d0a0d0a4c7561744f532d4169725f56343033325f524441383931305f5454535f4e4f4c56474c5f464c4f41540d0a0d0a4f4b0d0a", "tool": "fastat", - "rc": 0, - "timestamp": "2026-08-08T10:56:05Z", "sanitized": true, "phase": "recognition", "config_section": "legacy-import", @@ -14,5 +11,15 @@ "vendor": "openluat", "platform": "unisoc", "model": "air724ug" - } + }, + "responses": [ + { + "response_hex": "4154490d0a0d0a4c7561744f532d4169725f56343033325f524441383931305f5454535f4e4f4c56474c5f464c4f41540d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:56:05Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] } diff --git a/testcases/recognition/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json b/testcases/recognition/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json index e3bacf1a..8861c869 100644 --- a/testcases/recognition/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json +++ b/testcases/recognition/openluat/unisoc/air724ug-c9340434/AT_CGMM-bb33552b.json @@ -3,10 +3,7 @@ "platform": "unknown", "model": "unknown", "command": "AT+CGMM", - "response_hex": "41542b43474d4d0d0a0d0a2b43474d4d3a20224169723732345547220d0a0d0a4f4b0d0a", "tool": "at", - "rc": 0, - "timestamp": "2026-08-08T10:56:05Z", "sanitized": true, "phase": "recognition", "config_section": "legacy-import", @@ -14,5 +11,15 @@ "vendor": "openluat", "platform": "unisoc", "model": "air724ug" - } + }, + "responses": [ + { + "response_hex": "41542b43474d4d0d0a0d0a2b43474d4d3a20224169723732345547220d0a0d0a4f4b0d0a", + "rc": 0, + "timestamp": "2026-08-08T10:56:05Z", + "scenario": "feedback-20260808", + "sequence": 0, + "capture_sequence": 0 + } + ] }