#!/bin/sh
#
# Trame test suite - smoke-test the installed trame against the live
# system. Runs read-only verbs and --dry-run/--explain mutations only,
# never touches the real config.
#
# Usage:
#   trame-testsuite           pretty colored human output
#   trame-testsuite --json    single JSON summary
#
# Log: $TRAME_TESTSUITE_LOG (default /var/log/trame-testsuite.log).
#
# Copyright (C) SliTaz GNU/Linux - BSD License
# Author: See AUTHORS files
#

# No set -u: makes optional env vars (DISPLAY) painful.

# Locate trame: prefer installed, else dev tree relative to $0.
if [ -x /usr/bin/trame ]; then
	TRAME=/usr/bin/trame
elif [ -x "$(dirname "$0")/trame" ]; then
	TRAME="$(dirname "$0")/trame"
else
	echo "trame-testsuite: trame binary not found" >&2
	exit 2
fi

: ${TRAME_TESTSUITE_LOG:=/var/log/trame-testsuite.log}

JSON=no
case "${1:-}" in
	--json) JSON=yes ;;
	-h|--help)
		cat << EOT

Usage: trame-testsuite [--json]

Smoke-test the installed trame. Runs read-only verbs and dry-run
mutations only - safe to run anytime.

  --json     emit a single JSON summary (machine mode)
  -h, --help this help

Log file: \$TRAME_TESTSUITE_LOG (default /var/log/trame-testsuite.log)

EOT
		exit 0 ;;
esac

# Colors (TTY only, human mode).
RED=""; GRN=""; YEL=""; BLU=""; BLD=""; DIM=""; RST=""
if [ -t 1 ] && [ "$JSON" = "no" ]; then
	RED=$(printf '\033[31m'); GRN=$(printf '\033[32m')
	YEL=$(printf '\033[33m'); BLU=$(printf '\033[34m')
	BLD=$(printf '\033[1m');  DIM=$(printf '\033[2m')
	RST=$(printf '\033[0m')
fi

PASS=0; FAIL=0; SKIP=0; TOTAL=0
SECTION=""
JSON_RESULTS=""
LOG_OK=yes

# Try opening the log file; if not writable, fall back to /tmp.
# touch is used (not redirection) because POSIX shells exit on a
# failed redirect in non-interactive mode.
if ! touch "$TRAME_TESTSUITE_LOG" 2>/dev/null; then
	TRAME_TESTSUITE_LOG="/tmp/trame-testsuite.log"
	touch "$TRAME_TESTSUITE_LOG" 2>/dev/null || LOG_OK=no
fi
# Truncate to start fresh.
[ "$LOG_OK" = "yes" ] && : > "$TRAME_TESTSUITE_LOG" 2>/dev/null

logf() {
	[ "$LOG_OK" = "yes" ] && \
		printf '%s\n' "$*" >> "$TRAME_TESTSUITE_LOG" 2>/dev/null
}

section() {
	SECTION="$1"
	[ "$JSON" = "no" ] && printf '\n%s[%s]%s\n' "${BLD}${BLU}" "$1" "$RST"
	logf ""
	logf "[$1]"
}

# check <label> <cmdline> <validator>
# validators:
#   ok          rc must be 0
#   rc:N        rc must be N
#   contains:S  rc=0 and output contains S
#   json        rc=0 and output starts with { or [ and ends with } or ]
check() {
	local label="$1" cmd="$2" v="$3" out rc status
	TOTAL=$((TOTAL + 1))
	out=$(eval "$cmd" 2>&1); rc=$?
	status=PASS
	case "$v" in
		ok)
			[ $rc -eq 0 ] || status=FAIL ;;
		rc:*)
			[ "$rc" -eq "${v#rc:}" ] || status=FAIL ;;
		contains:*)
			[ $rc -eq 0 ] || status=FAIL
			case "$out" in *"${v#contains:}"*) ;; *) status=FAIL ;; esac ;;
		json)
			[ $rc -eq 0 ] || status=FAIL
			local first last
			first=$(printf '%s' "$out" | cut -c1)
			last=$(printf '%s' "$out" | tail -c 1)
			case "$first" in '{'|'[') ;; *) status=FAIL ;; esac
			case "$last"  in '}'|']') ;; *) status=FAIL ;; esac ;;
	esac

	if [ "$status" = "PASS" ]; then
		PASS=$((PASS + 1))
		if [ "$JSON" = "no" ]; then
			printf '  %sPASS%s %s\n' "$GRN" "$RST" "$label"
		fi
	else
		FAIL=$((FAIL + 1))
		if [ "$JSON" = "no" ]; then
			printf '  %sFAIL%s %s\n' "$RED" "$RST" "$label"
			printf '         %scmd:%s %s\n' "$DIM" "$RST" "$cmd"
			printf '         %src:%s  %s\n' "$DIM" "$RST" "$rc"
			# Trim long output.
			local short
			short=$(printf '%s' "$out" | head -c 200)
			printf '         %sout:%s %s\n' "$DIM" "$RST" "$short"
		fi
	fi
	logf "  $status  $label  (rc=$rc)"
	logf "    cmd: $cmd"
	logf "    out: $(printf '%s' "$out" | head -c 200)"
	if [ "$JSON" = "yes" ]; then
		# Escape minimal for JSON (label is hand-written, safe).
		JSON_RESULTS="${JSON_RESULTS}{\"section\":\"$SECTION\",\"label\":\"$label\",\"status\":\"$status\",\"rc\":$rc},"
	fi
}

skip() {
	local label="$1" why="$2"
	TOTAL=$((TOTAL + 1)); SKIP=$((SKIP + 1))
	if [ "$JSON" = "no" ]; then
		printf '  %sSKIP%s %s  %s(%s)%s\n' "$YEL" "$RST" "$label" \
			"$DIM" "$why" "$RST"
	fi
	logf "  SKIP  $label  ($why)"
	if [ "$JSON" = "yes" ]; then
		JSON_RESULTS="${JSON_RESULTS}{\"section\":\"$SECTION\",\"label\":\"$label\",\"status\":\"SKIP\",\"reason\":\"$why\"},"
	fi
}

#
# Header
#
TRAME_VER=$($TRAME --version 2>/dev/null | awk '{print $2}')
logf "trame-testsuite started $(date -u +%Y-%m-%dT%H:%M:%SZ)"
logf "trame: $TRAME (version $TRAME_VER)"
logf "host:  $(uname -srm)"

if [ "$JSON" = "no" ]; then
	printf '%strame-testsuite%s\n' "$BLD" "$RST"
	printf '  trame:   %s\n' "$TRAME"
	printf '  version: %s\n' "$TRAME_VER"
	printf '  host:    %s\n' "$(uname -srm)"
	printf '  log:     %s\n' "$TRAME_TESTSUITE_LOG"
fi

#
# Dispatcher
#
section dispatcher
check "--version"        "$TRAME --version"               "ok"
check "--help"           "$TRAME --help"                  "ok"
check "unknown cmd rc=1" "$TRAME bogus_xyz"               "rc:1"
check "unknown flag rc=1" "$TRAME system info --bogus"    "rc:1"

#
# Schema validity (every sub-command).
#
section schemas
for sub in system hostname service time network wifi locale log user storage kernel power display audio; do
	check "$sub --schema" "$TRAME $sub --schema" "contains:\"command\":\"$sub\""
done

#
# Per-subcommand smoke
#
section system
check "info --json"      "$TRAME system info --json"      "contains:\"kernel\":"
check "kernel --json"    "$TRAME system kernel --json"    "json"
check "uptime --json"    "$TRAME system uptime --json"    "json"

section hostname
check "get"              "$TRAME hostname get"            "ok"
check "get --json"       "$TRAME hostname get --json"     "contains:\"hostname\":"
check "set --explain"    "$TRAME hostname set demo --explain" "contains:\"explain\":["
check "set --dry-run"    "$TRAME hostname set demo --dry-run --json" "contains:\"dry_run\":true"

section service
check "list"             "$TRAME service list"            "ok"
check "list --json"      "$TRAME service list --json"     "json"

section time
check "get --json"       "$TRAME time get --json"         "json"
check "tz --json"        "$TRAME time tz --json"          "json"

section network
check "show --json"      "$TRAME network show --json"     "json"

section wifi
if [ -d /sys/class/net ]; then
	check "list --json"   "$TRAME wifi list --json"        "json"
	check "status --json" "$TRAME wifi status --json"      "json"
else
	skip  "list"          "no /sys/class/net"
fi

section locale
check "get"              "$TRAME locale get"              "ok"
check "list --json"      "$TRAME locale list --json"      "json"

section log
check "show --json"      "$TRAME log show --json"         "json"
check "clear --explain"  "$TRAME log clear --explain"     "contains:\"explain\":["

section user
check "list --json"      "$TRAME user list --json"        "json"

section storage
check "list"             "$TRAME storage list"            "ok"

section kernel
if [ -r /proc/modules ]; then
	check "modules list --json" "$TRAME kernel modules list --json" "json"
else
	skip  "modules list"  "no /proc/modules"
fi
if [ -r /proc/sys/kernel/hostname ]; then
	check "params get kernel.hostname" \
		"$TRAME kernel params get kernel.hostname --json" \
		"contains:\"key\":\"kernel.hostname\""
else
	skip  "params get"    "no /proc/sys (chroot?)"
fi
check "modules load --explain (dry plan)" \
	"$TRAME kernel modules load dummy --explain" \
	"contains:\"explain\":["

section power
if [ -r /sys/power/state ]; then
	check "suspend --explain" \
		"$TRAME power suspend --explain" \
		"contains:\"explain\":["
else
	skip  "suspend"       "no /sys/power/state"
fi
if [ -d /sys/devices/system/cpu/cpu0/cpufreq ]; then
	check "governor list --json" "$TRAME power governor list --json" "json"
	check "governor get --json"  "$TRAME power governor get --json"  "json"
else
	skip  "governor list" "no cpufreq sysfs"
	skip  "governor get"  "no cpufreq sysfs"
fi

section display
# Probe: command exists AND can actually connect to an X server.
if command -v xrandr >/dev/null 2>&1 && \
   [ -n "${DISPLAY:-}" ] && xrandr --query >/dev/null 2>&1; then
	check "list --json"    "$TRAME display list --json"   "json"
else
	skip  "list"           "no working X server (DISPLAY/xrandr)"
fi
if ls /sys/class/backlight/*/brightness >/dev/null 2>&1; then
	check "brightness get --json" \
		"$TRAME display brightness get --json" \
		"contains:\"percent\":"
else
	skip  "brightness get" "no /sys/class/backlight"
fi

section audio
if command -v amixer >/dev/null 2>&1; then
	# amixer needs an ALSA control state to read; treat amixer-not-ready
	# as skip rather than fail.
	if amixer sget Master >/dev/null 2>&1; then
		check "get --json"   "$TRAME audio get --json"    "contains:\"percent\":"
	else
		skip  "get"          "amixer cannot read Master"
	fi
else
	skip  "get"            "no amixer"
fi
if [ -r /proc/asound/cards ]; then
	check "devices --json" "$TRAME audio devices --json"  "json"
else
	skip  "devices"        "no /proc/asound/cards"
fi

#
# Summary
#
logf ""
logf "summary: pass=$PASS fail=$FAIL skip=$SKIP total=$TOTAL"

if [ "$JSON" = "yes" ]; then
	# Trim trailing comma.
	JSON_RESULTS=${JSON_RESULTS%,}
	printf '{"trame":"%s","version":"%s","results":[%s],"summary":{"pass":%d,"fail":%d,"skip":%d,"total":%d}}\n' \
		"$TRAME" "$TRAME_VER" "$JSON_RESULTS" \
		"$PASS" "$FAIL" "$SKIP" "$TOTAL"
else
	printf '\n'
	if [ "$FAIL" -eq 0 ]; then
		printf '%sOK%s    %d pass, %d skip (%d total)\n' \
			"${BLD}${GRN}" "$RST" "$PASS" "$SKIP" "$TOTAL"
	else
		printf '%sFAIL%s  %d pass, %s%d fail%s, %d skip (%d total)\n' \
			"${BLD}${RED}" "$RST" "$PASS" "$RED" "$FAIL" "$RST" "$SKIP" "$TOTAL"
	fi
fi

[ "$FAIL" -eq 0 ]
