#!/bin/bash # test_netmgrd.sh — meow_netmgrd 宿主桩测试(假 sysfs/procfs 节点 + 桩后端命令,MEOW_* env 覆盖)。 # 用法:./test_netmgrd.sh [host二进制路径](默认 ./meow_netmgrd_host;可传 mipsel 二进制 + QEMU 前缀) # QEMU="qemu-mipsel" ./test_netmgrd.sh ./meow_netmgrd # qemu 验证 set -u cd "$(dirname "$0")" BIN="${1:-./meow_netmgrd_host}" Q="${QEMU:-}" T=$(mktemp -d /tmp/netmgrdtest.XXXXXX) trap 'kill $DPID 2>/dev/null; rm -rf "$T"' EXIT PASS=0; FAIL=0 ck() { # ck <名> <期望子串> <实际> if echo "$3" | grep -qF "$2"; then PASS=$((PASS+1)); echo "PASS $1"; else FAIL=$((FAIL+1)); echo "FAIL $1: 期望含 [$2] 实际=[$3]"; fi } # ---- 搭假节点 ---- NET="$T/net"; RF="$T/rfkill"; BT="$T/bt" mkdir -p "$NET/lo" "$NET/wlan0" "$RF/rfkill0" "$RF/rfkill1" "$BT/hci0" echo up > "$NET/lo/operstate" echo up > "$NET/wlan0/operstate" echo wlan > "$RF/rfkill0/type"; echo 0 > "$RF/rfkill0/soft"; echo 0 > "$RF/rfkill0/hard" echo bluetooth > "$RF/rfkill1/type"; echo 1 > "$RF/rfkill1/soft"; echo 0 > "$RF/rfkill1/hard" echo 00:1A:7D:DA:71:13 > "$BT/hci0/address" echo on > "$BT/hci0/power" cat > "$T/netdev" <<'EOF' Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1000 10 0 0 0 0 0 0 1000 10 0 0 0 0 0 0 wlan0: 5000000 5000 0 0 0 0 0 0 1000000 1000 0 0 0 0 0 0 EOF cat > "$T/route" <<'EOF' Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT wlan0 005EC20A 6F3DC20A 0003 0 0 0 00FFFFFF 0 0 0 lo 0100007F 00000000 0001 0 0 0 000000FF 0 0 0 EOF printf 'nameserver 223.5.5.5\nnameserver 10.78.193.1\n' > "$T/resolv" echo meow > "$T/boot_mode" # ---- 期2 观测层打桩:/proc/net/{tcp,udp}(IPv4 LE hex)+ 域名规则 conf + amsd/svcd 缺省 ---- cat > "$T/tcp" <<'EOF' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 0: 32C14E0A:5137 0905CD8C:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 12345 1 0 0 10 0 1: 32C14E0A:4F32 32C34E0A:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 12346 1 0 0 10 0 2: 32C14E0A:0A00 00000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 67890 1 0 0 10 0 3: 32C14E0A:4F33 32C34E0A:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 0 1 0 0 10 0 EOF cat > "$T/udp" <<'EOF' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 0: 0100007F:D301 0100007F:0035 07 00000000:00000000 00:00000000 00000000 0 0 99999 1 0 0 10 0 EOF printf 'app:dev.player.ncm domain=-ads.example.com\nproc:dropbear domain=+x\n' > "$T/policy" # 桩后端命令(popen stubbing) cat > "$T/wpa_cli" <<'EOF' #!/bin/sh case "$1" in status) echo "wpa_state=COMPLETED"; echo "ssid=MyWiFi"; echo "ip_address=10.78.193.50"; echo "signal=-45";; scan) echo OK;; scan_results) printf 'bssid / frequency / signal level / flags / ssid\nxx:xx / 2412 / -50 / WPA2 / TestAP\n';; list_networks) printf 'network id / ssid / bssid / flags\n0\tMyWiFi\tany\t[CURRENT]\n';; add_network) echo 1;; set_network) echo OK;; enable_network) echo OK;; select_network) echo OK;; disconnect) echo OK;; esac exit 0 EOF cat > "$T/hciconfig" <<'EOF' #!/bin/sh echo "hci0: Type: Primary Bus: USB" echo " BD Address: 00:1A:7D:DA:71:13 ACL MTU: 1021:8 SCO MTU: 64:1" echo " UP RUNNING PSCAN" EOF cat > "$T/bluetoothctl" <<'EOF' #!/bin/sh case "${1:-}" in paired|paired-devices) echo "Device 00:1A:7D:DA:71:13 My Headset" echo "Device 11:22:33:44:55:66 Speaker";; devices) echo "Device 00:1A:7D:DA:71:13 My Headset" echo "Device 11:22:33:44:55:66 Speaker" echo "Device 33:DE:2E:21:AD:30 Pudding";; connect|disconnect|pair|remove) echo "Controller 00:1A:7D:DA:71:13 Connected: yes";; *) echo "Device 00:1A:7D:DA:71:13 My Headset"; echo "Device 11:22:33:44:55:66 Speaker";; esac exit 0 EOF cat > "$T/hcitool" <<'EOF' #!/bin/sh echo " < ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER" echo " < ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE" exit 0 EOF cat > "$T/bluealsa-cli" <<'EOF' #!/bin/sh if [ "${1:-}" = "a2dp" ]; then echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:" echo " Type: A2DP Source" echo " Codec: SBC" echo " Sampling: 44100 Hz" echo " Channels: 2" echo " Bits per Sample: 16" echo " State: RUNNING" echo " Acquired: yes" fi exit 0 EOF cat > "$T/ip" <<'EOF' #!/bin/sh for a in "$@"; do if [ "$a" = "wlan0" ]; then echo "2: wlan0 inet 10.78.193.50/24 brd 255.255.255.255 scope global wlan0"; fi if [ "$a" = "lo" ]; then echo "1: lo inet 127.0.0.1/8 scope host lo"; fi done exit 0 EOF for f in wpa_cli hciconfig bluetoothctl hcitool bluealsa-cli ip; do chmod +x "$T/$f"; done mkdir -p "$T/logs" "$T/pid" # 期2:DNS 禁用(宿主无 root 不绑 53),观测层经 MEOW_TCP/UDP 打桩;amsd/svcd 缺省(实体归因走 proc:comm→unknown)。 ENVS="MEOW_NET_DIR=$NET MEOW_NETDEV=$T/netdev MEOW_NET_ROUTE=$T/route MEOW_RESOLV=$T/resolv MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT MEOW_BOOT_MODE=$T/boot_mode MEOW_BOOTTS_CACHE=$T/bootts MEOW_IP=$T/ip MEOW_WPA_CLI=$T/wpa_cli MEOW_HCICONFIG=$T/hciconfig MEOW_BLUETOOTHCTL=$T/bluetoothctl MEOW_HCITOOL=$T/hcitool MEOW_BLUEALSA_CLI=$T/bluealsa-cli MEOW_BT_SCAN_CACHE=$T/bt_scan.txt MEOW_NET_SOCK=$T/net.sock MEOW_NET_POLL_SEC=1 MEOW_LOG_DIR=$T/logs MEOW_TCP=$T/tcp MEOW_UDP=$T/udp MEOW_NET_POLICY=$T/policy MEOW_AMS_SOCK=$T/no_ams MEOW_SVCD_SOCK=$T/no_svcd MEOW_DNS_DISABLE=1" ln -sf "$(readlink -f "$BIN")" "$T/meow_net" # argv[0] 双形态 → CLI # ---- T0: 守护起 ---- env $ENVS $Q "$BIN" --sock "$T/net.sock" >"$T/daemon.log" 2>&1 & DPID=$! for i in $(seq 1 30); do [ -S "$T/net.sock" ] && break; sleep 0.2; done [ -S "$T/net.sock" ] || { echo "FAIL T0: daemon socket 未出现"; cat "$T/daemon.log"; exit 1; } PASS=$((PASS+1)); echo "PASS T0 daemon up" # ---- T1: status(读级,任意调用者)---- OUT=$(env $ENVS $Q "$T/meow_net" status 2>&1) ck T1a "mode=meow src=daemon" "$OUT" ck T1b "iface:lo kind=lo up=true" "$OUT" ck T1c "iface:wlan0 kind=wifi up=true" "$OUT" ck T1d "ip=10.78.193.50" "$OUT" # 若 iface 行 ip 解析成功(ip 桩已修)——实际由 wlan gw 行给,见下 ck T1e "gw=10.194.61.111" "$OUT" ck T1f "wifi connected=1 state=COMPLETED ssid=MyWiFi" "$OUT" ck T1g "bt available=1 powered=1 address=00:1A:7D:DA:71:13 paired=2" "$OUT" ck T1h "rfkill:wlan soft=0 hard=0" "$OUT" ck T1i "rfkill:bluetooth soft=1 hard=0" "$OUT" # ---- T2: iface 单/全 ---- OUT=$(env $ENVS $Q "$T/meow_net" iface wlan0 2>&1) ck T2a '"name":"wlan0"' "$OUT" ck T2b '"kind":"wifi"' "$OUT" ck T2c '"ip":"10.78.193.50"' "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" iface 2>&1) ck T2d '"name":"lo"' "$OUT" # ---- T3: wifi networks(读级)---- OUT=$(env $ENVS $Q "$T/meow_net" wifi networks 2>&1) ck T3a "MyWiFi" "$OUT" # ---- T4: wifi scan(写级;宿主 CLI=CALL_OTHER → 拒)---- OUT=$(env $ENVS $Q "$T/meow_net" wifi scan 2>&1) ck T4a "permission denied" "$OUT" # ---- T5: bt status / bt paired(读级)---- OUT=$(env $ENVS $Q "$T/meow_net" bt status 2>&1) ck T5a "available=1 powered=1" "$OUT" ck T5b "paired=2" "$OUT" ck T5b2 "connected=2" "$OUT" # P1:内核 ACL 权威已连接数 OUT=$(env $ENVS $Q "$T/meow_net" bt paired 2>&1) ck T5c "My Headset" "$OUT" ck T5d "Speaker" "$OUT" # ---- T5e: bt connected(P1:内核 ACL 权威,名经 devices 缓存兜底)---- OUT=$(env $ENVS $Q "$T/meow_net" bt connected 2>&1) ck T5e "Device 33:DE:2E:21:AD:30 Pudding" "$OUT" ck T5e2 "Device 11:22:33:44:55:66 Speaker" "$OUT" # ---- T5f: bt a2dp(P1:bluealsa-cli a2dp list → codec/rate/acquired/role)---- OUT=$(env $ENVS $Q "$T/meow_net" bt a2dp 2>&1) ck T5f "codec=SBC" "$OUT" ck T5f2 "rate=44100" "$OUT" ck T5f3 "acquired=1" "$OUT" ck T5f4 "role=src" "$OUT" # ---- T5g: bt devices(P1:netmgrd 权威扫描缓存)---- cat > "$T/bt_scan.txt" <<'EOF' [NEW] Device 33:DE:2E:21:AD:30 Pudding [NEW] Device AA:BB:CC:DD:EE:FF Earbuds [DEL] Device 00:11:22:33:44:55 EOF OUT=$(env $ENVS $Q "$T/meow_net" bt devices 2>&1) ck T5g "Device 33:DE:2E:21:AD:30 Pudding" "$OUT" ck T5g2 "Device AA:BB:CC:DD:EE:FF Earbuds" "$OUT" # ---- T5h: bt 写级(connect/pair 等)在宿主 CLI=CALL_OTHER → 拒(仅系统身份+MeowOS 放行,见 T6)---- OUT=$(env $ENVS $Q "$T/meow_net" bt connect 33:DE:2E:21:AD:30 2>&1); RC=$? ck T5h "permission denied" "$OUT" ck T5h2 "rc=1" "rc=$RC" OUT=$(env $ENVS $Q "$T/meow_net" bt scan on 2>&1) ck T5i "permission denied" "$OUT" # ---- T6: HibyOS 只读降级(读可查,写拒)---- echo hiby > "$T/boot_mode" sleep 2 # 等守护下一拍热切换 OUT=$(env $ENVS $Q "$T/meow_net" status 2>&1) ck T6a "mode=readonly" "$OUT" ck T6b "iface:wlan0" "$OUT" # 读侧仍可查 OUT=$(env $ENVS $Q "$T/meow_net" wifi networks 2>&1) ck T6c "MyWiFi" "$OUT" # 读级子命令仍可用 OUT=$(env $ENVS $Q "$T/meow_net" wifi on 2>&1); RC=$? ck T6d "permission denied" "$OUT" echo meow > "$T/boot_mode" sleep 2 OUT=$(env $ENVS $Q "$T/meow_net" status 2>&1) ck T6e "mode=meow" "$OUT" # 热切换回全功能 # ---- T7: 守护日志(每次开机新文件;接口 add/写拒绝 必记)---- sleep 1 LOGS=$(ls "$T/logs"/net_*.log 2>/dev/null | head -1) [ -n "$LOGS" ] && { PASS=$((PASS+1)); echo "PASS T7a log file: $(basename "$LOGS")"; } || { FAIL=$((FAIL+1)); echo "FAIL T7a 无日志文件"; } [ -n "$LOGS" ] && { grep -q "iface add kind=wifi name=wlan0" "$LOGS" && { PASS=$((PASS+1)); echo "PASS T7b iface add 已记"; } || { FAIL=$((FAIL+1)); echo "FAIL T7b 未记 iface add"; }; } # ---- T8: 守护停 → CLI help 本地可用;status 报守护不可达 ---- kill $DPID 2>/dev/null; wait $DPID 2>/dev/null; DPID="" sleep 0.5 OUT=$(env $ENVS $Q "$T/meow_net" help 2>&1) ck T8a "usage: meow_net" "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" status 2>&1) ck T8b "守护不可达" "$OUT" # ---- T9: 无参 → help;未知子命令 → usage ---- OUT=$(env $ENVS $Q "$T/meow_net" 2>&1) ck T9a "usage: meow_net" "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" bogus 2>&1); RC=$? ck T9b "usage: meow_net" "$OUT" # ---- 重启 daemon A(T8 已停)供期2 观测/连接表/日志测试 ---- env $ENVS $Q "$BIN" --sock "$T/net.sock" >"$T/daemon2.log" 2>&1 & DPID=$! for i in $(seq 1 30); do [ -S "$T/net.sock" ] && break; sleep 0.2; done [ -S "$T/net.sock" ] || { echo "FAIL T10-pre: daemon 未重启"; cat "$T/daemon2.log"; exit 1; } # ---- T10: 期2 conns(读级;观测层经 MEOW_TCP/UDP 桩 + amsd/svcd 缺省 → proc:unknown 归因)---- sleep 1 # 等守护普查一拍 OUT=$(env $ENVS $Q "$T/meow_net" conns 2>&1) ck T10a "proc:unknown" "$OUT" ck T10b "10.78.193.50:20791" "$OUT" ck T10b2 "140.205.5.9:443" "$OUT" ck T10c "ESTAB" "$OUT" ck T10d "UDP" "$OUT" ck T10e "connection" "$OUT" # ---- T11: apps(实体聚合)---- OUT=$(env $ENVS $Q "$T/meow_net" apps 2>&1) ck T11a "entity" "$OUT" ck T11b "proc:unknown" "$OUT" # ---- T12: dns status/stats(DNS 禁用 → 显示 disabled)---- OUT=$(env $ENVS $Q "$T/meow_net" dns status 2>&1) ck T12a "proxy disabled" "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" dns stats 2>&1) ck T12b "proxy disabled" "$OUT" # ---- T13: dns flush(写级;宿主 CLI=CALL_OTHER → 拒)---- OUT=$(env $ENVS $Q "$T/meow_net" dns flush 2>&1) ck T13a "permission denied" "$OUT" # ---- T13b: 端到端 JSON 合法性(STATUS_0xE0 / IFACE_0xE2 可被完整解析,括号配对/无尾逗号)---- JOUT=$(python3 - "$T/net.sock" <<'PYEOF' import socket,struct,sys,json s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.connect(sys.argv[1]); s.settimeout(2) def recvn(n): d=b"" while len(d)&1) ck T15a "10.78.195.50:443" "$OUT" # inode=0 的连接仍在表中 OUT=$(env $ENVS $Q "$T/meow_net" apps 2>&1) ck T15b "proc:unknown" "$OUT" echo "$OUT" | grep -qE '^[0-9][0-9.]*:' && { FAIL=$((FAIL+1)); echo "FAIL T15c apps 出现原始端点行"; } || { PASS=$((PASS+1)); echo "PASS T15c no raw endpoint row"; } # ---- T16: policy(读级 show 任意调用者;写级仅系统身份 → 宿主 CLI=CALL_OTHER 拒)---- OUT=$(env $ENVS $Q "$T/meow_net" policy show 2>&1) ck T16a "app:dev.player.ncm" "$OUT" ck T16b "domain=" "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" policy set app:dev.player.ncm allow 2>&1); RC=$? ck T16c "permission denied" "$OUT" [ $RC -eq 1 ] && { PASS=$((PASS+1)); echo "PASS T16d rc=1"; } || { FAIL=$((FAIL+1)); echo "FAIL T16d rc=$RC"; } OUT=$(env $ENVS $Q "$T/meow_net" policy domain app:dev.player.ncm +x 2>&1) ck T16e "permission denied" "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" policy del app:dev.player.ncm 2>&1) ck T16f "permission denied" "$OUT" # ---- T17: SDK 自愿通道 end-to-end(meow_net_policy_check/report 对守护 A)---- SDK_BIN="$T/test_net_ipc" if [ -n "$Q" ]; then "$HOME/Project/R3ProII/.tools/zig-mipsel-cc" -Os -Wall -Wextra -I../sdk -I../sdk/include -I../meow_launcherd \ ../sdk/test_net_ipc.c ../sdk/meow_net_ipc.c -o "$SDK_BIN" 2>/dev/null else cc -Os -Wall -Wextra -I../sdk -I../sdk/include -I../meow_launcherd \ ../sdk/test_net_ipc.c ../sdk/meow_net_ipc.c -o "$SDK_BIN" 2>/dev/null fi if [ -x "$SDK_BIN" ]; then OUT=$(env MEOW_LOG_APP=dev.player.ncm MEOW_NET_SOCK="$T/net.sock" $Q "$SDK_BIN" 2>&1) ck T17a "CHECK_GOOD=0" "$OUT" # music.163.com 放行 ck T17b "CHECK_BLOCK=1" "$OUT" # ads.example.com 被策略 deny → block ck T17c "REPORT_DONE" "$OUT" else FAIL=$((FAIL+1)); echo "FAIL T17 SDK test 构建失败" fi # ---- T18: SDK report → status JSON entity_bytes 回读 ---- JOUT2=$(python3 - "$T/net.sock" <<'PYEOF' import socket,struct,sys,json s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.connect(sys.argv[1]); s.settimeout(2) def recvn(n): d=b"" while len(d)=0(signal_poll 桩已给)---- OUT=$(env $ENVS $Q "$T/meow_net" status 2>&1) ck T18d "rssi=-45" "$OUT" # ---- T14: DNS 代理 wire 实测(守护 B:高端口 + 桩上游 + 全局 deny;测 deny/forward/cache)---- HOSTIP=$(hostname -I 2>/dev/null | awk '{print $1}') [ -n "$HOSTIP" ] || HOSTIP="127.0.0.1" cat > "$T/policy_dns" <<'EOF' global domain=-ads.example.com dns.filter_aaaa=1 EOF printf 'nameserver %s\n' "$HOSTIP" > "$T/resolv_dns" cat > "$T/dns_test.py" <<'PYEOF' import socket, struct, threading, sys, os UP_IP = os.environ.get("T_UP_IP", "0.0.0.0") up = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) up.bind((UP_IP, 5311)) # 0.0.0.0 通配,绑到宿主自身 IP up.settimeout(6) def serve(): try: while True: d,a = up.recvfrom(1536) off=12 while d[off]!=0: off += 1+d[off] off += 1+4 qsec = d[12:off] resp = d[:2] + b'\x81\x80' + b'\x00\x01\x00\x01\x00\x00\x00\x00' + qsec resp += b'\xc0\x0c' + b'\x00\x01\x00\x01' + struct.pack('>I',60) + b'\x00\x04' + socket.inet_aton('11.22.33.44') up.sendto(resp, a) except Exception: pass threading.Thread(target=serve, daemon=True).start() def query(name, qtype=1): s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(3) hdr=b'\x12\x34'+b'\x01\x00'+b'\x00\x01\x00\x00\x00\x00\x00\x00' q=b''.join(bytes([len(l)])+l.encode() for l in name.split('.'))+b'\x00'+struct.pack('>HH',qtype,1) s.sendto(hdr+q, ("127.0.0.1", 5300)) r,_=s.recvfrom(1536); rco=r[3]&0xf; an=struct.unpack('>H',r[6:8])[0] return rco,an try: d1,a1 = query("ads.example.com") # deny → NXDOMAIN (rcode 3) d2,a2 = query("music.163.com") # allow → forward → A 11.22.33.44 da,aa = query("music.163.com", 28) # AAAA + dns.filter_aaaa=1 → 空应答(回落 A) sys.stdout.write("DENY_RCODE=%d\nALLOW_AN=%d\nAAAA_RCODE=%d AAAA_AN=%d\n" % (d1, a2, da, aa)) except Exception as e: sys.stdout.write("ERR=%s\n" % e) PYEOF # 追加式覆写(env 左→右,后者胜出):DNS 启用、换独立 socket/resolv/policy、上游端口。 DNSENV="$ENVS MEOW_DNS_DISABLE=0 MEOW_NET_SOCK=$T/dns.sock MEOW_RESOLV=$T/resolv_dns MEOW_NET_POLICY=$T/policy_dns MEOW_DNS_PORT=5300 MEOW_DNS_UPSTREAM_PORT=5311" ln -sf "$(readlink -f "$BIN")" "$T/dns_netmgrd" env $DNSENV $Q "$BIN" --sock "$T/dns.sock" >"$T/dnsd.log" 2>&1 & D2PID=$! for i in $(seq 1 30); do [ -S "$T/dns.sock" ] && break; sleep 0.2; done sleep 1 OUT=$(env $DNSENV $Q "$T/meow_net" dns status 2>&1) ck T14a "enabled=1 upstream=$HOSTIP" "$OUT" WIRE=$(python3 "$T/dns_test.py" 2>&1) ck T14b "DENY_RCODE=3" "$WIRE" ck T14c "ALLOW_AN=1" "$WIRE" ck T14c2 "AAAA_RCODE=0" "$WIRE" # 期4 AAAA 过滤:rcode 0 ck T14c3 "AAAA_AN=0" "$WIRE" # 且 0 条应答(回落 A) OUT=$(env $DNSENV $Q "$T/meow_net" dns stats 2>&1) ck T14d "q=3" "$OUT" # deny + allow + AAAA = 3 查询 ck T14e "deny=1" "$OUT" kill $D2PID 2>/dev/null; wait $D2PID 2>/dev/null # 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认) grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; } echo "==============================" echo "PASS=$PASS FAIL=$FAIL" [ $FAIL -eq 0 ]