| # shellcheck shell=bash |
| # tests/e2e/lib.sh — shared helpers for the doriscli end-to-end test harness. |
| # |
| # Sourced by start-testing.sh (which runs under bash). Provides: |
| # - result tracking (PASS / FAIL / SKIP) and the final summary |
| # - _run_dcli: the single place that invokes the doriscli binary, in either |
| # * stateless mode (DORIS_HOST/USER/... exported — no config files touched), or |
| # * file mode (DORIS_* unset, isolated $HOME — exercises ~/.doris config) |
| # - assertion helpers built on jq: expect_json / expect_ok / expect_err / skip |
| # |
| # Design notes that the assertions depend on (verified against doriscli source): |
| # - Every command prints a bare JSON value to stdout and exits 0 on success, |
| # non-zero on error (error text on stderr). |
| # - `auth status` ALWAYS exits 0; real connectivity is in the .mysql_status field. |
| # - profile get --full/--raw and profile diff REQUIRE the FE HTTP API; the default |
| # `profile get` summary has a SQL fallback; `profile history` needs audit_log. |
| # Those are gated on HTTP_OK / detected at runtime and recorded as SKIP, not FAIL. |
| |
| # ---- result state -------------------------------------------------------- |
| N_PASS=0 |
| N_FAIL=0 |
| N_SKIP=0 |
| FAILED_NAMES=() |
| SKIPPED_NAMES=() |
| |
| # Populated by _run_dcli on every call: |
| OUT="" # captured stdout |
| ERR="" # captured stderr |
| RC=0 # exit code |
| |
| # ---- colors (disabled when not a tty or NO_COLOR set) -------------------- |
| if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then |
| C_GREEN=$'\033[32m'; C_RED=$'\033[31m'; C_YELLOW=$'\033[33m' |
| C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_RESET=$'\033[0m' |
| else |
| C_GREEN=""; C_RED=""; C_YELLOW=""; C_BOLD=""; C_DIM=""; C_RESET="" |
| fi |
| |
| # ---- logging ------------------------------------------------------------- |
| # log: to console AND the run log file. logfile_only: just the file. |
| log() { printf '%s\n' "$*"; printf '%s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true; } |
| logfile_only() { printf '%s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true; } |
| |
| # Collapse a (possibly multi-line) string to a single, truncated line for the |
| # console summary. Full untruncated output always lives in $LOG_FILE. |
| _oneline() { |
| printf '%s' "$1" | tr '\n\t' ' ' | cut -c1-240 |
| } |
| |
| # ---- result recorders ---------------------------------------------------- |
| record_pass() { |
| N_PASS=$((N_PASS + 1)) |
| log " ${C_GREEN}PASS${C_RESET} $1" |
| } |
| record_fail() { |
| N_FAIL=$((N_FAIL + 1)) |
| FAILED_NAMES+=("$1") |
| log " ${C_RED}FAIL${C_RESET} $1" |
| [ -n "$2" ] && log " ${C_DIM}↳ $(_oneline "$2")${C_RESET}" |
| } |
| record_skip() { |
| N_SKIP=$((N_SKIP + 1)) |
| SKIPPED_NAMES+=("$1") |
| log " ${C_YELLOW}SKIP${C_RESET} $1" |
| [ -n "$2" ] && log " ${C_DIM}↳ $(_oneline "$2")${C_RESET}" |
| } |
| |
| # Print a section banner for a suite. |
| suite_banner() { |
| log "" |
| log "${C_BOLD}━━ $1 ━━${C_RESET}" |
| } |
| |
| # ---- the doriscli runner ------------------------------------------------- |
| # _run_dcli <args...> |
| # Runs $BIN with the configured global flags (--init-sql / --socks5) prepended, |
| # in stateless mode by default. Set DCLI_STATELESS=0 before calling to exercise |
| # the file-based config path (DORIS_* unset, $HOME isolated to $ISOLATED_HOME). |
| # Captures stdout->OUT, stderr->ERR, exit code->RC. Logs the invocation. |
| _run_dcli() { |
| local globals=() |
| [ -n "${CFG_INIT_SQL:-}" ] && globals+=(--init-sql "$CFG_INIT_SQL") |
| [ -n "${CFG_SOCKS5:-}" ] && globals+=(--socks5 "$CFG_SOCKS5") |
| |
| local errf; errf="$(mktemp "${TMPDIR:-/tmp}/dcli-err.XXXXXX")" |
| |
| if [ "${DCLI_STATELESS:-1}" = "1" ]; then |
| OUT=$(HOME="$ISOLATED_HOME" \ |
| DORIS_HOST="$CFG_HOST" \ |
| DORIS_PORT="$CFG_PORT" \ |
| DORIS_HTTP_PORT="$CFG_HTTP_PORT" \ |
| DORIS_USER="$CFG_USER" \ |
| DORIS_PASSWORD="$CFG_PASSWORD" \ |
| "$BIN" "${globals[@]}" "$@" 2>"$errf") |
| RC=$? |
| else |
| # File mode: scrub any DORIS_* the caller's shell may carry, so stateless |
| # mode is NOT triggered, and point HOME at the isolated config dir. |
| OUT=$(HOME="$ISOLATED_HOME" \ |
| env -u DORIS_HOST -u DORIS_USER -u DORIS_PASSWORD \ |
| -u DORIS_PORT -u DORIS_HTTP_PORT -u DORIS_ENV -u DORIS_INIT_SQL \ |
| "$BIN" "${globals[@]}" "$@" 2>"$errf") |
| RC=$? |
| fi |
| |
| ERR=$(cat "$errf" 2>/dev/null); rm -f "$errf" |
| |
| { |
| printf '\n$ doriscli' |
| printf ' %q' "${globals[@]}" "$@" |
| printf ' [stateless=%s]\n' "${DCLI_STATELESS:-1}" |
| printf 'rc=%s\n--- stdout ---\n%s\n--- stderr ---\n%s\n' "$RC" "$OUT" "$ERR" |
| } >>"$LOG_FILE" 2>/dev/null || true |
| } |
| |
| # ---- assertion helpers --------------------------------------------------- |
| # All of these record exactly one PASS or FAIL (or SKIP) result. |
| |
| # expect_json "<name>" '<jq filter>' <doriscli args...> |
| # Appends --format json, expects exit 0 and a jq filter that evaluates truthy. |
| # Pass '' as the filter to only require valid JSON + exit 0. |
| expect_json() { |
| local name="$1" filter="$2"; shift 2 |
| _run_dcli --format json "$@" |
| if [ "$RC" -ne 0 ]; then |
| record_fail "$name" "exit=$RC; stderr: $ERR" |
| return |
| fi |
| if ! printf '%s' "$OUT" | jq -e . >/dev/null 2>&1; then |
| record_fail "$name" "stdout is not valid JSON: $OUT" |
| return |
| fi |
| if [ -n "$filter" ] && ! printf '%s' "$OUT" | jq -e "$filter" >/dev/null 2>&1; then |
| record_fail "$name" "assertion failed [ $filter ]; got: $OUT" |
| return |
| fi |
| record_pass "$name" |
| } |
| |
| # expect_parsed "<name>" [jq-args...] '<filter>' |
| # Assert a jq filter against the LAST captured $OUT (from a prior _run_dcli), |
| # WITHOUT re-running the command. The profile suite fetches one real profile and |
| # then makes many assertions on it; re-running per assertion would re-hit the FE |
| # and risk the profile being evicted mid-suite. Extra jq args (e.g. |
| # `--arg q "$qid"`) may precede the filter. Records exactly one PASS or FAIL. |
| expect_parsed() { |
| local name="$1"; shift |
| if printf '%s' "$OUT" | jq -e "$@" >/dev/null 2>&1; then |
| record_pass "$name" |
| else |
| record_fail "$name" "assertion failed [ $* ]; got: $(_oneline "$OUT")" |
| fi |
| } |
| |
| # expect_ok "<name>" <doriscli args...> — just requires exit 0 (raw, no --format). |
| expect_ok() { |
| local name="$1"; shift |
| _run_dcli "$@" |
| if [ "$RC" -eq 0 ]; then record_pass "$name" |
| else record_fail "$name" "exit=$RC; stderr: $ERR"; fi |
| } |
| |
| # expect_err "<name>" <doriscli args...> — requires NON-zero exit. |
| # Optional: set EXPECT_ERR_MATCH to a substring required in stderr. |
| expect_err() { |
| local name="$1"; shift |
| _run_dcli "$@" |
| if [ "$RC" -eq 0 ]; then |
| record_fail "$name" "expected non-zero exit, got 0; stdout: $OUT" |
| return |
| fi |
| if [ -n "${EXPECT_ERR_MATCH:-}" ] && ! printf '%s' "$ERR" | grep -qiF "$EXPECT_ERR_MATCH"; then |
| record_fail "$name" "exit ok but stderr missing '$EXPECT_ERR_MATCH': $ERR" |
| return |
| fi |
| record_pass "$name" |
| } |
| |
| # expect_stdout_contains "<name>" "<substr>" <doriscli args...> |
| # Raw run (no --format), exit 0, and stdout contains substr (for --help, table/csv). |
| expect_stdout_contains() { |
| local name="$1" needle="$2"; shift 2 |
| _run_dcli "$@" |
| if [ "$RC" -ne 0 ]; then |
| record_fail "$name" "exit=$RC; stderr: $ERR" |
| return |
| fi |
| if printf '%s' "$OUT" | grep -qF "$needle"; then record_pass "$name" |
| else record_fail "$name" "stdout missing '$needle': $(_oneline "$OUT")"; fi |
| } |
| |
| # skip "<name>" "<reason>" — record a SKIP (precondition absent, not a bug). |
| skip() { record_skip "$1" "$2"; } |
| |
| # jget '<jq filter>' — echo a scalar pulled from the last OUT (raw -r). Empty on error. |
| jget() { printf '%s' "$OUT" | jq -r "$1" 2>/dev/null; } |
| |
| # _capture_profile_fixture — persist the current `profile get --raw` $OUT (a JSON |
| # string holding the raw profile text) as the offline parser regression fixture. |
| # Doing it from a live run means the fixture is always a REAL Doris profile. |
| # Committing the generated file activates the cargo test in |
| # src/parser/profile_parser.rs (which is a visible no-op until the file exists). |
| _capture_profile_fixture() { |
| local dir="$REPO_ROOT/tests/e2e/fixtures" |
| mkdir -p "$dir" 2>/dev/null || return 0 |
| if printf '%s' "$OUT" | jq -r . > "$dir/sample_profile.txt" 2>/dev/null; then |
| log " ${C_DIM}captured real profile -> tests/e2e/fixtures/sample_profile.txt (commit it to activate the offline parser regression test)${C_RESET}" |
| fi |
| } |
| |
| # ---- final summary ------------------------------------------------------- |
| print_summary() { |
| local total=$((N_PASS + N_FAIL + N_SKIP)) |
| log "" |
| log "${C_BOLD}══════════════════════ SUMMARY ══════════════════════${C_RESET}" |
| log " total: $total ${C_GREEN}pass: $N_PASS${C_RESET} ${C_RED}fail: $N_FAIL${C_RESET} ${C_YELLOW}skip: $N_SKIP${C_RESET}" |
| if [ "$N_SKIP" -gt 0 ]; then |
| log "" |
| log "${C_YELLOW}Skipped (precondition not met on this cluster — not a failure):${C_RESET}" |
| local s; for s in "${SKIPPED_NAMES[@]}"; do log " - $s"; done |
| fi |
| if [ "$N_FAIL" -gt 0 ]; then |
| log "" |
| log "${C_RED}${C_BOLD}Failed tests:${C_RESET}" |
| local f; for f in "${FAILED_NAMES[@]}"; do log " ${C_RED}✗${C_RESET} $f"; done |
| log "" |
| log "Full command output for every test is in:" |
| log " $LOG_FILE" |
| log "${C_RED}${C_BOLD}RESULT: FAIL ($N_FAIL failing)${C_RESET}" |
| else |
| log "" |
| log "Full run log: $LOG_FILE" |
| log "${C_GREEN}${C_BOLD}RESULT: PASS${C_RESET}" |
| fi |
| } |