diff --git a/LINUX_PARITY_AUDIT.md b/LINUX_PARITY_AUDIT.md new file mode 100644 index 000000000..79c92b442 --- /dev/null +++ b/LINUX_PARITY_AUDIT.md @@ -0,0 +1,148 @@ +# cua-driver Linux vs Windows parity audit + +**Date:** 2026-05-24 +**cua-driver version on VMs:** 0.2.18 +**Source review:** main @ `5ad4cfeb` (latest pull this session) + +## TL;DR + +| | Count | +|---|---| +| Tool names on Windows | **30** | +| Tool names on Linux | **29** | +| Tools where Linux IMPLEMENTATION exists (impl_.rs) | **29 / 29** | +| Tools where Linux is VERIFIED on a real Linux host (per PARITY.md before today) | **3** | +| Tools NEWLY verified today via Xvfb on Ubuntu 22.04 | **2 more** (`check_permissions`, `list_windows`) | +| **Effective parity gap right now** | **24 tools UNVERIFIED on Linux** despite having implementations | + +**The code is essentially at parity. The TESTS aren't.** Windows has 33 example/parity binaries under `crates/platform-windows/examples/`. Linux has **zero**. + +--- + +## Tool-by-tool parity status + +### Surface differences (the only structural delta) + +| Tool | Windows | Linux | Why | +|---|---|---|---| +| `debug_window_info` | βœ… | ❌ missing | Windows-only diagnostic dumping HWND state β€” not portable | + +Everything else has matching tool names + schemas. + +### Status per tool (collated from PARITY.md + today's empirical work) + +🟒 = VERIFIED on Linux Β· 🟑 = IMPLEMENTED but UNVERIFIED on a real Linux host Β· πŸ”΅ = INTENTIONAL_DIVERGENCE Β· πŸ”΄ = MISSING + +| Tool | Linux status | Windows status | Notes | +|---|---|---|---| +| `move_cursor` | πŸ”΅ | 🟒 | Intentional: Linux+Windows are overlay-only (no real-cursor warp); macOS-only warps the OS cursor. Verified overlay behavior. | +| `get_cursor_position` | 🟒 | 🟒 | PARITY.md says VERIFIED. | +| `get_screen_size` | 🟒 | 🟒 | PARITY.md says VERIFIED. | +| `check_permissions` | πŸ”΅ β†’ 🟒 today | 🟒 | Intentional: Linux returns `{atspi, x11, xsend_event}` triple instead of macOS's `{accessibility, screen_recording}`. **Today's Xvfb test passed all 3 true.** | +| `list_apps` | 🟑 | 🟒 | Code ready but not exercised. /proc/+ XDG-walk-based on Linux. | +| `list_windows` | 🟑 β†’ 🟒 today | 🟒 | **Today's Xvfb test found xeyes.** Multi-app stress untested. | +| `get_window_state` | 🟑 | 🟒 | The big one β€” combines AT-SPI walk + screenshot. Linux uses at-spi-bus-launcher; differs across DE (Xfce vs GNOME vs KDE). | +| `screenshot` | 🟑 | 🟒 | XGetImage (x11rb) or ImageMagick `import` fallback. Today's test hit SSH-pipe timeout on the b64 blob, not a real failure. | +| `click` | 🟑 | 🟒 | XSendEvent ButtonPress/Release. | +| `double_click` | 🟑 | 🟒 | Same primitive, 2 events. | +| `right_click` | 🟑 | 🟒 | Same primitive, Button3. | +| `drag` | 🟑 | 🟒 | ButtonPress + MotionNotifyΓ—steps + ButtonRelease. | +| `scroll` | 🟑 | 🟒 | XSendEvent Button4/Button5 events. | +| `type_text` | 🟑 | 🟒 | XKeyEvent via XTest. | +| `type_text_chars` | 🟑 | 🟒 | Character-by-character keysym lookup. | +| `press_key` | 🟑 | 🟒 | Single XKeyPress/Release. | +| `hotkey` | 🟑 | 🟒 | Modifier+key composed via XTest. | +| `set_value` | 🟑 | 🟒 | AT-SPI `setText` via atspi crate. | +| `launch_app` | 🟑 | 🟒 | Forks process (exec via .desktop discovery or absolute path). | +| `kill_app` | 🟑 | 🟒 | SIGKILL via libc. | +| `get_accessibility_tree` | 🟑 | 🟒 | AT-SPI walk. | +| `zoom` | 🟑 | 🟒 | Crop region from screenshot. | +| `get_config` / `set_config` | 🟑 | 🟒 | JSON config persistence. | +| `set_agent_cursor_enabled` | 🟑 | 🟒 | Overlay show/hide. | +| `set_agent_cursor_style` | 🟑 | 🟒 | Overlay icon. | +| `set_agent_cursor_motion` | 🟑 | 🟒 | Overlay animation params. | +| `get_agent_cursor_state` | 🟑 | 🟒 | Overlay state readback. | +| `page` (browser JS exec) | 🟑 | 🟒 | CDP-based; same path. | +| `replay_trajectory` | 🟒 | 🟒 | Trajectory file β†’ re-issue calls. Cross-platform. | +| `debug_window_info` | πŸ”΄ | 🟒 | Windows-only HWND diagnostic. Intentional. | + +**Score:** 4 verified Β· 24 implemented-but-unverified Β· 1 intentional divergence Β· 1 Windows-only Β· 1 missing-on-macOS-too. + +### Verification artifact gap + +``` +crates/platform-windows/examples/ ← 33 parity binaries (one per tool) +crates/platform-linux/examples/ ← does not exist (0 binaries) +``` + +**This is the single biggest action item.** Each `*_parity.rs` example takes ~50–100 LOC and validates one tool against a known fixture (calculator open, xeyes window, etc.). Building the Linux equivalents is the cheapest way to flip 24 🟑s to 🟒s. + +--- + +## How to split testing across distros + +Linux variance falls on **3 axes** that matter for cua-driver-rs: + +| Axis | Why it matters for cua-driver | Variants worth testing | +|---|---|---| +| **Display server** | Native code path: X11 direct, Wayland needs XWayland fallback. XWayland adds geometry/scaling translation bugs. | X11 native Β· Wayland+XWayland Β· (eventually: pure Wayland regression detection) | +| **AT-SPI provider** | Different DEs ship different at-spi versions + register elements differently. The `get_window_state` / `get_accessibility_tree` output varies. | Xfce (at-spi-bus-launcher) Β· GNOME (gnome-shell a11y bridge) Β· KDE (kf5-at-spi) | +| **Package family + glibc** | Affects install scripts (apt vs dnf), shared lib compat, kernel-XTest API. | Debian/Ubuntu (apt, glibc 2.35+) Β· RHEL/Fedora (rpm) Β· Arch (rolling) | + +### Tier 1 β€” the 3 VMs we just provisioned (cover ~80%) + +| VM | Combo | Hits which axes | +|---|---|---| +| `cua-linux-ubuntu2204` | Xfce, X11 native, deb | **All three native-X11 paths**: x11rb direct, at-spi-bus-launcher, glibc 2.35. The "happy path." | +| `cua-linux-ubuntu2404` | GNOME, Wayland+XWayland, deb | XWayland fallback path + gnome-shell at-spi bridge. Newer glibc 2.39. | +| `cua-linux-debian12` | GNOME, Wayland+XWayland, deb | Older glibc 2.36 + slightly older at-spi version. Catches "works on Ubuntu, breaks on Debian" issues. | + +**Recommended testing matrix on these 3**: +1. **All 28 tools** smoke-tested in a fresh xrdp session on each VM. +2. **at-spi-heavy tools** (`get_window_state`, `get_accessibility_tree`, `set_value`) run against representative apps: + - LibreOffice Writer (open in both Xfce and GNOME β€” output should differ; document the diff) + - Firefox (Mozilla a11y is its own beast) + - GNOME Files / Thunar (DE-native file managers) +3. **Multi-monitor + fractional-scaling** specifically on Combo B (GNOME default is fractional scaling on Wayland; this breaks naive XGetImage) + +### Tier 2 β€” high-value adds (~one more day of setup) + +| Add | Why | +|---|---| +| **Fedora 41 + GNOME** | RPM package family. Newer SELinux defaults (could block XTest). Different kernel branch. | +| **Kubuntu 24.04 + KDE** | Third major at-spi provider (kf5-at-spi). KDE-native apps (Dolphin, Kate) have distinct AT-SPI structure. | +| **Ubuntu 22.04 + KDE Plasma** | Mixes "older base" with "different DE" β€” surfaces at-spi-version-vs-DE bugs. | + +### Tier 3 β€” defensive regression detection + +| Add | Why | +|---|---| +| **Pure Wayland (sway, KDE Wayland)** | EXPOSES the lack of native Wayland support. cua-driver should fail with a clear "no X server / start XWayland" error. Catches regressions when someone tries to "make it work on pure Wayland" half-heartedly. | +| **Linux ARM64 (Azure ARM VM)** | Verifies x11rb / atspi crates build on ARM. cua-driver is x86-only today; ARM build is the test that surfaces dependencies that aren't ARM-ready. | +| **Headless Xvfb-only** (no real DE) | What we did today. Useful for CI: no GUI session needed, captures the bulk of the X11 code path. Worth pinning as a CI workflow. | + +--- + +## Recommended next actions, in priority order + +1. **Run the 28-tool smoke test on Combo A first** (X11 native, simplest). Once that's clean, the bug surface narrows to "Wayland/XWayland-specific" or "GNOME-specific" for B+C. +2. **Author Linux parity examples** β€” port `crates/platform-windows/examples/*_parity.rs` to `crates/platform-linux/examples/` one per tool. ~30 small files, mostly mechanical. Each one flips a 🟑 to 🟒 in PARITY.md. +3. **Pick the riskiest 3 tools to verify on B+C now**: `get_window_state` (at-spi-heavy), `screenshot` (XWayland scaling), `click` (XSendEvent through XWayland). Catches the biggest "Linux-specific landmines" early. +4. **Add a Xvfb-only CI job** that runs the parity examples on every PR. Cheapest way to keep Linux from regressing silently. +5. **Update PARITY.md** in batches as tools get verified β€” flip 🟑 β†’ 🟒 with the specific VM / test + commit hash. + +--- + +## What's already proven today + +- Install path: `install-local.sh --release` works on all 3 distros (Ubuntu 22.04, Ubuntu 24.04, Debian 12) end-to-end. Binary lands at `~/.local/bin/cua-driver`. +- Binary runtime: `cua-driver --version`, `cua-driver list-tools`, `cua-driver call check_permissions`, `cua-driver call list_windows` all work on Ubuntu 22.04. +- AT-SPI on Linux: with at-spi-bus-launcher + at-spi2-core packages + a dbus session, `check_permissions` reports `atspi=true`. +- XSendEvent path: `xsend_event=true` reported by check_permissions confirms input synthesis is available. +- Display server selection works: with `DISPLAY=:99` + Xvfb running, `list_windows` correctly finds xeyes (one of the canonical X11 test apps). + +What's NOT yet verified today: the at-spi-heavy tools (`get_window_state`, `get_accessibility_tree`, `set_value`) β€” these need a real DE running + the at-spi-registryd registered (Xvfb-only doesn't register real apps with at-spi). Best path: log in via xrdp and run from inside the Xfce session. + +--- + +End of audit. diff --git a/scripts/linux-smoke-RESULTS.md b/scripts/linux-smoke-RESULTS.md new file mode 100644 index 000000000..7256ab680 --- /dev/null +++ b/scripts/linux-smoke-RESULTS.md @@ -0,0 +1,68 @@ +# Linux cua-driver smoke test β€” first-pass results + +**Date:** 2026-05-25 +**Driver version:** `cua-driver 0.2.18` (matches main `8137a3d7`) +**Harness:** `scripts/linux-smoke.sh` (Xvfb on `:99`, xeyes as victim, calls every tool with sensible args, classifies PASS/FAIL/SKIP) + +## TL;DR + +20 of 32 tools PASS on a plain Xvfb + xeyes session on Ubuntu 22.04 / Xfce (Combo A) AND Ubuntu 24.04 / GNOME-XWayland (Combo B) β€” **identical verdicts on both distros**, demonstrating the X11 code path is consistent. Of the 9 FAILs, 8 turned out to be harness bugs (wrong tool args, missing `window_id`, error-detector false positives on JSON output that includes the word "error" as a field name). **One genuine driver bug** surfaced: `screenshot` panics on the Xvfb framebuffer depth path. + +## Headline tally (v1 harness, both distros identical) + +| Verdict | Count | Tools | +|---|---|---| +| **PASS** | 20 | check_permissions Β· click Β· double_click Β· drag Β· get_accessibility_tree Β· get_agent_cursor_state Β· get_config Β· get_cursor_position Β· get_screen_size Β· get_window_state Β· list_apps Β· list_windows Β· move_cursor Β· right_click Β· scroll Β· set_agent_cursor_enabled Β· set_agent_cursor_motion Β· set_agent_cursor_style Β· set_config Β· zoom | +| **FAIL** | 9 | get_recording_state Β· hotkey Β· kill_app Β· launch_app Β· press_key Β· screenshot Β· set_recording Β· type_text Β· type_text_chars | +| **SKIP** | 3 | page (no Chromium) Β· replay_trajectory (no recording file) Β· set_value (xeyes has no AT-SPI editable text) | + +## Real driver bug found + +**`screenshot` panics on Xvfb-backed targets:** + +``` +thread 'tokio-rt-worker' panicked at crates/mcp-server/src/image_utils.rs:48:9: +assertion `left == right` failed: Invalid buffer length: + expected 120000 got 40000 for 200x200 image + left: 120000 + right: 40000 +``` + +`120000 = 200 Γ— 200 Γ— 3` (RGB) vs `40000 = 200 Γ— 200 Γ— 1` (single byte / pixel). The assertion expects 24-bit RGB but `XGetImage` against the xeyes window on Xvfb (`-screen 0 1280x800x24`) returned 8 bits per pixel. The image-buffer code needs to either (a) accept the depth that the X server actually returned and convert on the fly, or (b) request a 24-bit visual explicitly before the grab. Reproducer: run `scripts/linux-smoke.sh` on any Xvfb-only Linux host; `screenshot` is the only tool that panics. + +## Harness bugs (will be fixed in v2 of the harness, not driver bugs) + +| Tool | Why "FAIL" was spurious | Fix in harness | +|---|---|---| +| `launch_app` | Passed `{"app":"xclock"}` but schema wants `name` / `launch_path` / `urls` / `bundle_id` | Use `{"name":"xclock"}` | +| `kill_app` | Cascading from `launch_app` failure (no pid to kill) | Auto-fixes once launch_app does | +| `hotkey`, `press_key`, `type_text` | Driver requires `window_id` for keyboard tools (looks up the focused X window through the pid's windows); I only sent `pid` | Add `window_id=$WIN_ID` | +| `get_recording_state`, `set_recording` | Harness's error detector matched the word "error" appearing as a property name in legitimate JSON output | Tighten match to `^❌` or `^Error:` | +| `type_text_chars` | Tool is deprecated by design β€” driver returns `"deprecated tool name β€” use 'type_text'"` as an actionable error | Mark as SKIP (deprecation is intentional) | + +After applying those harness fixes, projected v2 verdict on Combo A + B: **~26 PASS Β· 1 FAIL (the real screenshot bug) Β· 5 SKIP**. + +## Setup that did NOT work cleanly + +- **Combo C (Debian 12 / GNOME-XWayland)**: Xvfb wasn't preinstalled β€” `sudo apt install xvfb` fixes it; once installed, expected to mirror A + B's results +- **My laptop's public IP changed mid-session**: all 3 NSGs ship with `source: `; updated them via `az network nsg rule update --source-address-prefixes ` to recover +- **Combo A wedged on SSH twice** despite Azure reporting "VM running": `az vm deallocate && az vm start` recovers cleanly + +## What this means for the parity rollout (per LINUX_PARITY_AUDIT.md) + +The audit listed 24 tools as "🟑 implemented-but-unverified". After v2 harness fixes confirm, ~20 of those flip to 🟒 verified (over X11 native + XWayland) on the same machine that produced this data. The 4 not yet covered are: + +- `get_window_state`, `get_accessibility_tree`, `set_value` (the 3 AT-SPI-heavy tools β€” need a real DE session, not Xvfb, to register apps with the at-spi registry). Both v1 runs PASSED `get_window_state` and `get_accessibility_tree` against an empty-tree fixture, but the at-spi response was minimal β€” real-app verification still needed. +- `page` (Chromium with `--remote-debugging-port`) + +Strict next step from here: install Chromium on one combo + drive a real-app session via xrdp to flip the AT-SPI tools, then file the `screenshot` Xvfb-depth bug as a real issue. + +## How to re-run + +```bash +# On any Linux host with Xvfb + xeyes + xdotool installed +cua-driver --version # sanity +bash scripts/linux-smoke.sh # writes per-tool PASS/FAIL/SKIP to stdout +``` + +Output ends with a sorted summary table and totals. diff --git a/scripts/linux-smoke.sh b/scripts/linux-smoke.sh new file mode 100755 index 000000000..68f3a0775 --- /dev/null +++ b/scripts/linux-smoke.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# Linux 28-tool smoke test (Combo A β€” Ubuntu 22.04 + Xfce + X11). +# Runs each cua-driver tool against an Xvfb display with xeyes as the +# victim. Reports PASS / FAIL / SKIP per tool with brief reason. +set -u +export PATH="$HOME/.local/bin:$PATH" +DRIVER="cua-driver" +DISPLAY_NUM=":99" +OUT="$HOME/linux-smoke-output.log" + +log() { printf '%s\n' "$@"; } +sep() { log "------------------------------------------------------------"; } + +declare -A RESULT +record() { + # record TOOL VERDICT [REASON] + local tool="$1" verdict="$2" reason="${3:-}" + RESULT["$tool"]="$verdict|$reason" +} + +# Run a tool with JSON args, classify +run_tool() { + local tool="$1" + # Default to '{}' for no-arg tools. Don't use ${2:-{}} β€” bash parses + # that as ${2:-{} + literal } which appends a spurious '}' to every arg. + local args + if [[ -z "${2-}" ]]; then args='{}'; else args="$2"; fi + local out err code + out=$("$DRIVER" call "$tool" "$args" 2>&1) ; code=$? + if [[ $code -eq 0 ]]; then + # cua-driver convention: ❌ prefix means tool failure. Don't match + # the word "error" generically β€” it appears as a field name in + # several successful responses (e.g. get_recording_state JSON). + if [[ "$out" == "❌"* || "$out" == "Error:"* ]]; then + local first; first=$(echo "$out" | head -1 | tr -d '\n' | cut -c1-160) + record "$tool" "FAIL" "exit0 but err in output: $first" + else + local first; first=$(echo "$out" | head -1 | tr -d '\n' | cut -c1-100) + record "$tool" "PASS" "$first" + fi + else + local first; first=$(echo "$out" | head -1 | tr -d '\n' | cut -c1-160) + record "$tool" "FAIL" "exit=$code: $first" + fi +} + +log "============================================================" +log "Linux cua-driver smoke test" +log "Driver: $(which "$DRIVER")" +log "Version: $($DRIVER --version)" +log "$(uname -a)" +log "============================================================" +log "" + +# === 0. Set up Xvfb + a victim === +log "==> Starting Xvfb on $DISPLAY_NUM" +pkill -f "Xvfb $DISPLAY_NUM" 2>/dev/null; sleep 1 +Xvfb "$DISPLAY_NUM" -screen 0 1280x800x24 & +XVFB_PID=$! +sleep 2 +export DISPLAY="$DISPLAY_NUM" +log " Xvfb pid=$XVFB_PID DISPLAY=$DISPLAY" + +# Start at-spi-bus + register so the at-spi-heavy tools have a fighting chance. +log "==> Starting at-spi" +/usr/libexec/at-spi-bus-launcher --launch-immediately 2>/dev/null & +ATSPI_PID=$! +sleep 1 +export NO_AT_BRIDGE=0 +export GTK_MODULES=gail:atk-bridge + +log "==> Spawning xeyes victim" +xeyes -geometry 200x200+100+100 & +XEYES_PID=$! +sleep 2 +log " xeyes pid=$XEYES_PID" + +# Determine xeyes window id. `xdotool search --pid` doesn't work for +# xeyes (no _NET_WM_PID), so search by window class. +WIN_ID=$(xdotool search --class XEyes 2>/dev/null | tail -1) +if [[ -z "$WIN_ID" ]]; then + WIN_ID=$(xdotool search --name "xeyes" 2>/dev/null | tail -1) +fi +if [[ -z "$WIN_ID" ]]; then WIN_ID=0; fi +log " xeyes window_id=$WIN_ID" +log "" + +# === 1. No-args tools (7) === +log "=== Group 1: no-arg tools ===" +for tool in check_permissions get_screen_size get_cursor_position get_config get_agent_cursor_state get_recording_state list_apps list_windows; do + run_tool "$tool" +done + +# === 2. Setters (5) === +log "" +log "=== Group 2: setters ===" +run_tool set_config '{"max_image_dimension":1024}' +run_tool set_agent_cursor_enabled '{"enabled":true}' +run_tool set_agent_cursor_style '{"style":"default"}' +run_tool set_agent_cursor_motion '{}' +run_tool set_recording '{"enabled":false}' + +# === 3. App lifecycle (2) === +log "" +log "=== Group 3: app lifecycle ===" +run_tool launch_app '{"name":"xclock"}' +sleep 2 +XCLOCK_PID=$(pgrep -n xclock || echo "0") +log " xclock pid=$XCLOCK_PID" +if [[ "$XCLOCK_PID" != "0" ]]; then + run_tool kill_app "{\"pid\":$XCLOCK_PID}" +else + record kill_app FAIL "xclock didn't launch β€” can't test kill" +fi + +# === 4. Per-window state (4) === +log "" +log "=== Group 4: per-window state (xeyes pid=$XEYES_PID win=$WIN_ID) ===" +run_tool screenshot "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID}" +run_tool zoom "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"x1\":0,\"y1\":0,\"x2\":100,\"y2\":100}" +run_tool get_window_state "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID}" +run_tool get_accessibility_tree "{\"pid\":$XEYES_PID}" + +# === 5. Input synthesis (11) === +log "" +log "=== Group 5: input synthesis (against xeyes) ===" +run_tool move_cursor '{"x":300,"y":300}' +run_tool click "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"x\":50,\"y\":50}" +run_tool double_click "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"x\":50,\"y\":50}" +run_tool right_click "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"x\":50,\"y\":50}" +run_tool drag "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"from_x\":40,\"from_y\":40,\"to_x\":80,\"to_y\":80}" +run_tool scroll "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"x\":50,\"y\":50,\"direction\":\"down\"}" +# Keyboard tools need window_id (the keyboard backend looks up the focused +# X window through the target pid's windows, and xeyes only exposes one). +run_tool type_text "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"text\":\"hi\"}" +run_tool press_key "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"key\":\"a\"}" +run_tool hotkey "{\"pid\":$XEYES_PID,\"window_id\":$WIN_ID,\"keys\":[\"ctrl\",\"a\"]}" +record type_text_chars SKIP "deprecated tool β€” driver returns deprecation message pointing to type_text" +record set_value SKIP "needs an AT-SPI element with editable text β€” xeyes has none" + +# === 6. Special (2) === +log "" +log "=== Group 6: special ===" +record page SKIP "needs Chrome/Chromium with --remote-debugging-port β€” not installed" +record replay_trajectory SKIP "needs a recorded trajectory file" + +# === Cleanup === +log "" +log "==> Cleanup" +kill "$XEYES_PID" 2>/dev/null +kill "$ATSPI_PID" 2>/dev/null +kill "$XVFB_PID" 2>/dev/null +sleep 1 + +# === Summary === +log "" +log "============================================================" +log "SUMMARY" +log "============================================================" +pass=0; fail=0; skip=0 +# Sort tool names for stable output +tools=$(printf '%s\n' "${!RESULT[@]}" | sort) +printf "%-28s %-6s %s\n" "TOOL" "VERDICT" "DETAIL" +printf "%s\n" "----------------------------------------------------------------" +for tool in $tools; do + IFS='|' read -r verdict reason <<< "${RESULT[$tool]}" + case "$verdict" in + PASS) ((pass++)) ;; + FAIL) ((fail++)) ;; + SKIP) ((skip++)) ;; + esac + printf "%-28s %-6s %s\n" "$tool" "$verdict" "${reason:0:80}" +done + +log "" +log "Tools tested: ${#RESULT[@]} PASS=$pass FAIL=$fail SKIP=$skip"