#!/usr/bin/env bash # 离线单元测试:验证 cellular-proxy 的探测链路永不选中 USB gadget 网卡。 # 用法: bash tests/detect-gadget.sh [repo路径] # 不依赖真实 /sys,靠覆盖探针函数模拟设备现场(usb0=configfs-gadget.g1, wwan0=bam-dmux)。 REPO="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" LIB="$REPO/scripts/lib.sh" [[ -f "$LIB" ]] || { echo "找不到 $LIB"; exit 1; } PASS=0 FAIL=0 check_eq() { local name="$1" want="$2" got="$3" if [[ "$want" == "$got" ]]; then PASS=$((PASS+1)); printf 'ok %s\n' "$name" else FAIL=$((FAIL+1)); printf 'FAIL %s (want=%s got=%s)\n' "$name" "$want" "$got" fi } # scenario # 在子 shell 里 source lib.sh,替换所有触碰真实系统的探针,然后 eval 代码。 scenario() { ( FAKE_IFACES="$1" FAKE_DRIVERS="$2" FAKE_IPS="$3" FAKE_DEFAULT="$4" FAKE_MM_IFACE="$5" FAKE_HAVE_MMCLI="$6" CODE="$7" # shellcheck disable=SC1090 source "$LIB" >/dev/null 2>&1 set +eu lookup() { local table="$1" key="$2" pair for pair in $table; do if [[ "${pair%%=*}" == "$key" ]]; then printf '%s' "${pair#*=}"; return 0; fi done return 1 } iface_exists() { local i for i in $FAKE_IFACES; do [[ "$i" == "$1" ]] && return 0; done return 1 } iface_driver() { lookup "$FAKE_DRIVERS" "$1" || true; } iface_has_carrier() { iface_exists "$1"; } detect_default_iface() { printf '%s' "$FAKE_DEFAULT"; } list_ifaces() { local i ip4 for i in $FAKE_IFACES; do ip4="$(lookup "$FAKE_IPS" "$i" || true)" printf '%s|up|%s\n' "$i" "$ip4" done } # 只需支持 ip -4 -o addr show dev X;ip route show default 一律返回空 ip() { local a prev="" dev="" v for a in "$@"; do [[ "$prev" == "dev" ]] && dev="$a" prev="$a" done [[ -n "$dev" ]] || return 0 v="$(lookup "$FAKE_IPS" "$dev" || true)" [[ -n "$v" ]] && printf '1: %s inet %s scope global %s\n' "$dev" "$v" "$dev" return 0 } if [[ "$FAKE_HAVE_MMCLI" == "yes" ]]; then mmcli() { case "$1" in -L) echo " /org/freedesktop/ModemManager1/Modem/0 [QUECTEL] EC20" ;; -m) echo " Bearer | paths: /org/freedesktop/ModemManager1/Bearer/0" ;; -b) printf ' IPv4 configuration | interface: %s\n' "$FAKE_MM_IFACE" ;; esac return 0 } else command() { if [[ "$1" == "-v" && "$2" == "mmcli" ]]; then return 1; fi builtin command "$@" } fi eval "$CODE" ) 2>/dev/null } IF_REAL="usb0 wlan0 wwan0" DRV_REAL="usb0=configfs-gadget.g1 wwan0=bam-dmux wlan0=brcmfmac" IPS_REAL="usb0=192.168.68.1/24 wwan0=10.2.8.250/30 wlan0=192.168.1.23/24" real() { scenario "$IF_REAL" "$DRV_REAL" "$IPS_REAL" wlan0 "${2:-}" "${3:-no}" "$1"; } check_eq "usb0 识别为 USB gadget" gadget \ "$(real 'iface_is_usb_gadget usb0 && echo gadget || echo no')" check_eq "wwan0 不被误判为 gadget" no \ "$(real 'iface_is_usb_gadget wwan0 && echo gadget || echo no')" check_eq "usb0 进入 skip 列表" skip \ "$(real 'is_virtual_or_skip_iface usb0 && echo skip || echo keep')" check_eq "bam-dmux 命中蜂窝驱动白名单" cell \ "$(real 'iface_looks_cellular_by_driver wwan0 && echo cell || echo no')" check_eq "gadget 驱动不算蜂窝" no \ "$(real 'iface_looks_cellular_by_driver usb0 && echo cell || echo no')" check_eq "usb0 评分为 0" 0 \ "$(real 'score_iface_as_cellular usb0 wlan0')" check_eq "wwan0 评分达到置信阈值" high \ "$(real 's=$(score_iface_as_cellular wwan0 wlan0); [[ $s -ge 70 ]] && echo high || echo low:$s')" check_eq "无 mmcli 时自动探测得 wwan0" wwan0 \ "$(real 'detect_cellular_iface ""')" check_eq "配置写死 usb0 被纠回 wwan0" wwan0 \ "$(real 'detect_cellular_iface usb0')" check_eq "旧 patterns 含 usb/enx 仍选 wwan0" wwan0 \ "$(real 'CELLULAR_IFACE_PATTERNS="wwan,usb,enx,cdc"; detect_cellular_iface ""')" check_eq "ModemManager bearer 优先采用" wwan0 \ "$(real 'detect_cellular_iface ""' wwan0 yes)" check_eq "ModemManager 报 gadget 时被忽略" wwan0 \ "$(real 'detect_cellular_iface ""' usb0 yes)" check_eq "resolve_cellular 拦截 usb0" wwan0 \ "$(real 'CELLULAR_IFACE=usb0; REQUIRE_CELLULAR_IFACE=true; resolve_cellular')" # 只有 gadget 可选时必须失败,而不是把 usb0 绑成出口 check_eq "仅 gadget 时 resolve_cellular 拒绝返回 usb0" die \ "$(scenario "usb0 wlan0" "usb0=configfs-gadget.g1 wlan0=brcmfmac" \ "usb0=192.168.68.1/24 wlan0=192.168.1.23/24" wlan0 "" no \ 'CELLULAR_IFACE=usb0; REQUIRE_CELLULAR_IFACE=true; out=$(resolve_cellular 2>/dev/null); rc=$?; if [[ $rc -ne 0 || -z $out ]]; then echo die; else echo "$out"; fi')" # verify.sh 的前置体检:出口绑在 gadget 上时必须 exit 4,而不是跑一堆 curl 再给含糊 FAIL verify_guard() { local sandbox conf rc sandbox="$(mktemp -d)" cp -r "$REPO"/. "$sandbox/" 2>/dev/null rm -rf "$sandbox/.git" mkdir -p "$sandbox/config" "$sandbox/etc" cat >"$sandbox/config/settings.conf" <>"$sandbox/scripts/lib.sh" <<'EOF' iface_is_usb_gadget() { [[ "$1" == usb0 || "$1" == rndis0 ]]; } EOF ( cd "$sandbox" && CONFIG_FILE="$sandbox/config/settings.conf" \ EGRESS_CHECK_URL="http://127.0.0.1:1/" EGRESS_TIMEOUT=1 VERIFY_QUICK=true \ bash scripts/verify.sh >/dev/null 2>&1 ) rc=$? rm -rf "$sandbox" printf '%s' "$rc" } check_eq "verify.sh 对 gadget 出口直接 exit 4" 4 "$(verify_guard usb0)" check_eq "verify.sh 对正常出口不报 exit 4" not4 \ "$(rc="$(verify_guard wwan0)"; [[ "$rc" == 4 ]] && echo 4 || echo not4)" # upgrade.sh 的迁移函数:旧 settings.conf 里的 usb/enx 必须被就地剔除 migrate_case() { local conf fn out conf="$(mktemp)" fn="$(mktemp)" printf 'CELLULAR_IFACE=wwan0\nCELLULAR_IFACE_PATTERNS=%s\nPROXY_MIXED_PORT=7890\n' "$1" >"$conf" # 从 upgrade.sh 里抽出 migrate_iface_patterns 函数体,避免执行整个升级脚本 awk '/^migrate_iface_patterns\(\) \{/,/^\}/' "$REPO/scripts/upgrade.sh" >"$fn" out="$( set +eu warn() { :; } # shellcheck disable=SC1090 source "$fn" migrate_iface_patterns "$conf" >/dev/null 2>&1 awk -F= '/^CELLULAR_IFACE_PATTERNS=/{print substr($0,index($0,"=")+1)}' "$conf" # 顺带确认其它行没被吃掉 grep -c '^PROXY_MIXED_PORT=7890$' "$conf" )" rm -f "$conf" "$fn" printf '%s' "$(printf '%s' "$out" | tr '\n' '/')" } check_eq "升级迁移剔除 usb/enx 且保留其它配置" "wwan,wwp,ppp,cdc/1" \ "$(migrate_case 'wwan,wwp,usb,enx,ppp,cdc')" check_eq "已干净的 patterns 不被改动" "wwan,wwp,ppp,cdc/1" \ "$(migrate_case 'wwan,wwp,ppp,cdc')" printf '\n通过 %d / 失败 %d\n' "$PASS" "$FAIL" [[ "$FAIL" -eq 0 ]]