test: operator_survival_test.sh — survival suite (группы 5-10)
5 FLOOD BUILD: 6 функций (3×Python + 3×Node.js) параллельно → все Ready 6 RAPID DISCARD STORM: 30 create→DELETE без build, orphan-check 7 CHURN UNDER FIRE: 10 PUT env-updates под 200 параллельными invokes, rate≥90% 8 PHOENIX: 3 цикла DELETE+recreate одного имени 9 SELF-HEALING MARATHON: 5×kubectl delete deployment → auto-recreate (RequeueAfter) 10 INVOKE HURRICANE: 500 параллельных invokes в 5 волнах (100 × 5), rate≥90% Ожидаемое время прогона: 75-100 минут
This commit is contained in:
@@ -0,0 +1,928 @@
|
||||
#!/usr/bin/env bash
|
||||
# 2026-03-21 — operator_survival_test.sh
|
||||
# ТЕСТ НА ВЫЖИВАНИЕ — суровые многоуровневые нагрузочные сценарии оператора.
|
||||
#
|
||||
# Группа 5: FLOOD BUILD — 6 функций параллельно (3 Python + 3 Node.js), все до Ready
|
||||
# Группа 6: RAPID DISCARD STORM — 30 create→DELETE без build, проверка на мусор
|
||||
# Группа 7: CHURN UNDER FIRE — 10 PUT с меняющимися env под 200 параллельными invokes
|
||||
# Группа 8: PHOENIX — 3 полных цикла DELETE+recreate одного имени
|
||||
# Группа 9: SELF-HEALING MARATHON — 5 раз вручную убить Deployment, ждать восстановления
|
||||
# Группа 10: INVOKE HURRICANE — 500 параллельных invokes в 5 волнах, считаем success rate
|
||||
#
|
||||
# Ожидаемое время прогона: 75-100 минут.
|
||||
#
|
||||
# ЗАПУСКАТЬ НА VM:
|
||||
# nohup bash ~/terra/sless/operator_survival_test.sh > /tmp/survival.log 2>&1 &
|
||||
# tail -f /tmp/survival.log
|
||||
#
|
||||
# Зависимости: curl, python3, zip, kubectl
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ─── CONFIG ──────────────────────────────────────────────────────────────────
|
||||
|
||||
TOKEN=$(cat /home/naeel/terra/sless/test.token)
|
||||
NS="sless-ffd1f598c169b0ae"
|
||||
API="https://sless.kube5s.ru/v1/namespaces/$NS"
|
||||
FN_BASE="https://sless.kube5s.ru/fn/$NS"
|
||||
DEPLOY_NS="sless-fn-$NS"
|
||||
# Уникальный префикс — не пересекается с prod-сервисами и lifecycle-тестом
|
||||
TS=$(date +%s)
|
||||
SFX="sv-${TS}" # sv = survival
|
||||
|
||||
# ─── СТАТИСТИКА (глобальная) ──────────────────────────────────────────────────
|
||||
|
||||
PASS=0; FAIL=0
|
||||
declare -A GRP_PASS GRP_FAIL # per-group counters
|
||||
|
||||
# ─── ЦВЕТА ───────────────────────────────────────────────────────────────────
|
||||
|
||||
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||
|
||||
# ─── HELPERS ─────────────────────────────────────────────────────────────────
|
||||
|
||||
_cur_grp="" # текущая группа для per-group счётчиков
|
||||
|
||||
pass() {
|
||||
echo -e " ${GREEN}[PASS]${RESET} $1"
|
||||
PASS=$((PASS + 1))
|
||||
[[ -n "$_cur_grp" ]] && GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 ))
|
||||
}
|
||||
fail() {
|
||||
echo -e " ${RED}[FAIL]${RESET} $1"
|
||||
FAIL=$((FAIL + 1))
|
||||
[[ -n "$_cur_grp" ]] && GRP_FAIL[$_cur_grp]=$(( ${GRP_FAIL[$_cur_grp]:-0} + 1 ))
|
||||
}
|
||||
info() { echo -e " ${CYAN}[INFO]${RESET} $1"; }
|
||||
warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; }
|
||||
section() {
|
||||
_cur_grp="G$1"
|
||||
echo -e "\n${BOLD}━━━ ГРУППА $1: $2 ━━━${RESET}"
|
||||
GRP_PASS[$_cur_grp]=0; GRP_FAIL[$_cur_grp]=0
|
||||
}
|
||||
|
||||
# api_code METHOD URL [body] → HTTP status code
|
||||
api_code() {
|
||||
local method="$1" url="$2" body="${3:-}"
|
||||
local args=(-s -o /dev/null -w "%{http_code}" -m 120
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||||
curl "${args[@]}" "$url"
|
||||
}
|
||||
|
||||
# api_json METHOD URL [body] → response body
|
||||
api_json() {
|
||||
local method="$1" url="$2" body="${3:-}"
|
||||
local args=(-s -m 120 -H "Authorization: Bearer $TOKEN")
|
||||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||||
curl "${args[@]}" "$url"
|
||||
}
|
||||
|
||||
check_code() {
|
||||
local label="$1" got="$2" want="$3"
|
||||
if [[ "$got" == "$want" ]]; then pass "$label → HTTP $got"
|
||||
else fail "$label → HTTP $got (ожидали $want)"; fi
|
||||
}
|
||||
|
||||
# svc_phase NAME → phase string
|
||||
svc_phase() {
|
||||
api_json GET "$API/services/$1" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null
|
||||
}
|
||||
|
||||
# wait_phase NAME WANT TIMEOUT_SEC → 0=ok 1=timeout 2=Failed
|
||||
wait_phase() {
|
||||
local name="$1" want="$2" timeout_sec="${3:-300}"
|
||||
local deadline=$((SECONDS + timeout_sec))
|
||||
while [[ $SECONDS -lt $deadline ]]; do
|
||||
local phase
|
||||
phase=$(svc_phase "$name")
|
||||
[[ "$phase" == "$want" ]] && return 0
|
||||
[[ "$phase" == "Failed" && "$want" != "Failed" ]] && return 2
|
||||
sleep 5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# wait_all_ready TIMEOUT NAME1 NAME2 ... → число сколько НЕ достигло Ready
|
||||
wait_all_ready() {
|
||||
local timeout_sec="$1"; shift
|
||||
local names=("$@")
|
||||
local deadline=$((SECONDS + timeout_sec))
|
||||
local not_ready=( "${names[@]}" )
|
||||
while [[ $SECONDS -lt $deadline && ${#not_ready[@]} -gt 0 ]]; do
|
||||
local still=()
|
||||
for n in "${not_ready[@]}"; do
|
||||
local p
|
||||
p=$(svc_phase "$n")
|
||||
[[ "$p" == "Ready" ]] || still+=("$n")
|
||||
done
|
||||
not_ready=( "${still[@]}" )
|
||||
[[ ${#not_ready[@]} -eq 0 ]] && break
|
||||
sleep 5
|
||||
done
|
||||
echo "${#not_ready[@]}" # количество не дошедших до Ready
|
||||
}
|
||||
|
||||
# make_python_zip NAME → /tmp/surv_py_{NAME}.zip
|
||||
# Python echo-handler: возвращает {"ok":true,"env":TEST_VAR,"grp":GRP_VAR}
|
||||
make_python_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
import os, json
|
||||
|
||||
def handle(event):
|
||||
return {
|
||||
"ok": True,
|
||||
"env": os.environ.get("TEST_VAR", ""),
|
||||
"grp": os.environ.get("GRP_VAR", ""),
|
||||
"msg": event.get("msg", ""),
|
||||
"iter": event.get("iter", 0),
|
||||
}
|
||||
PYEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/surv_py_${name}.zip" handler.py)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# make_node_zip NAME → /tmp/surv_node_{NAME}.zip
|
||||
make_node_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.js" <<'JSEOF'
|
||||
'use strict';
|
||||
exports.handle = function(event) {
|
||||
return {
|
||||
ok: true,
|
||||
env: process.env.TEST_VAR || '',
|
||||
grp: process.env.GRP_VAR || '',
|
||||
msg: (event && event.msg) || '',
|
||||
iter: (event && event.iter) || 0,
|
||||
};
|
||||
};
|
||||
JSEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/surv_node_${name}.zip" handler.js)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# upload_zip NAME ZIPFILE → HTTP code
|
||||
upload_zip() {
|
||||
local name="$1" zipfile="$2"
|
||||
curl -s -o /dev/null -w "%{http_code}" -m 120 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "code=@$zipfile" \
|
||||
"$API/services/$name/upload"
|
||||
}
|
||||
|
||||
# create_py_deploy NAME MEMORY ENV_VARS_JSON — create + upload python handler
|
||||
# return: 0=ok, 1=fail
|
||||
create_py_deploy() {
|
||||
local name="$1" mem="${2:-128}" envj="${3:-{}}"
|
||||
local http
|
||||
http=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$name\",\"display_name\":\"survival test\",\
|
||||
\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":$mem,\"env_vars\":$envj}")
|
||||
[[ "$http" == "201" ]] || { fail "create $name → HTTP $http"; return 1; }
|
||||
make_python_zip "$name"
|
||||
local ucode
|
||||
ucode=$(upload_zip "$name" "/tmp/surv_py_${name}.zip")
|
||||
[[ "$ucode" == "200" ]] || { fail "upload $name → HTTP $ucode"; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
# create_node_deploy NAME MEMORY ENV_VARS_JSON — create + upload node handler
|
||||
create_node_deploy() {
|
||||
local name="$1" mem="${2:-128}" envj="${3:-{}}"
|
||||
local http
|
||||
http=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$name\",\"display_name\":\"survival test\",\
|
||||
\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":$mem,\"env_vars\":$envj}")
|
||||
[[ "$http" == "201" ]] || { fail "create $name → HTTP $http"; return 1; }
|
||||
make_node_zip "$name"
|
||||
local ucode
|
||||
ucode=$(upload_zip "$name" "/tmp/surv_node_${name}.zip")
|
||||
[[ "$ucode" == "200" ]] || { fail "upload $name → HTTP $ucode"; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
# invoke_one NAME → HTTP code
|
||||
invoke_one() {
|
||||
local name="$1"
|
||||
curl -s -o /dev/null -w "%{http_code}" -m 30 \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{"msg":"survival"}' \
|
||||
"$FN_BASE/$name"
|
||||
}
|
||||
|
||||
# invoke_burst NAME COUNT PARALLEL — запустить COUNT запросов пачками PARALLEL
|
||||
# выводит "SUCCESS=N FAIL=M"
|
||||
invoke_burst() {
|
||||
local name="$1" total="$2" parallel="$3"
|
||||
local results_dir
|
||||
results_dir=$(mktemp -d)
|
||||
local idx=0
|
||||
while [[ $idx -lt $total ]]; do
|
||||
local batch_pids=()
|
||||
local batch=0
|
||||
while [[ $batch -lt $parallel && $idx -lt $total ]]; do
|
||||
(
|
||||
local code
|
||||
code=$(invoke_one "$name")
|
||||
echo "$code" > "$results_dir/${idx}"
|
||||
) &
|
||||
batch_pids+=($!)
|
||||
idx=$((idx + 1)); batch=$((batch + 1))
|
||||
done
|
||||
wait "${batch_pids[@]}"
|
||||
done
|
||||
local s=0 f=0
|
||||
for file in "$results_dir"/*; do
|
||||
local c; c=$(cat "$file")
|
||||
if [[ "$c" == "200" ]]; then s=$((s + 1)); else f=$((f + 1)); fi
|
||||
done
|
||||
rm -rf "$results_dir"
|
||||
echo "SUCCESS=$s FAIL=$f"
|
||||
}
|
||||
|
||||
# deploy_ns_has NAME → 0 если Deployment существует
|
||||
deploy_ns_has() {
|
||||
kubectl get deployment -n "$DEPLOY_NS" "$1" --no-headers 2>/dev/null | grep -q "$1"
|
||||
}
|
||||
|
||||
# cleanup NAME — удалить сервис (teardown)
|
||||
cleanup() {
|
||||
local name="$1"
|
||||
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
|
||||
sleep 2
|
||||
}
|
||||
|
||||
# Список для teardown — заполняется по ходу тестов
|
||||
CLEANUP_NAMES=()
|
||||
teardown() {
|
||||
if [[ ${#CLEANUP_NAMES[@]} -gt 0 ]]; then
|
||||
echo -e "\n${CYAN}[TEARDOWN]${RESET} удаляю тестовые сервисы..."
|
||||
for name in "${CLEANUP_NAMES[@]}"; do
|
||||
cleanup "$name"
|
||||
echo -e " ${CYAN}[TEARDOWN]${RESET} удалён: $name"
|
||||
done
|
||||
fi
|
||||
}
|
||||
trap teardown EXIT
|
||||
|
||||
# ─── СТАРТОВЫЙ БАННЕР ────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||||
echo -e "${BOLD}║ OPERATOR SURVIVAL TEST — sless v0.1.49 ║${RESET}"
|
||||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||||
echo -e " Дата : $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo -e " API : $API"
|
||||
echo -e " SFX : $SFX"
|
||||
echo ""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section 5 "FLOOD BUILD — 6 функций параллельно (3 × Python + 3 × Node.js)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: убедиться что много одновременных builds не мешают друг другу,
|
||||
# все достигают Ready, все отвечают на invoke.
|
||||
|
||||
FB1="fb1-${SFX}"; FB2="fb2-${SFX}"; FB3="fb3-${SFX}"
|
||||
FB4="fb4-${SFX}"; FB5="fb5-${SFX}"; FB6="fb6-${SFX}"
|
||||
FB_ALL=("$FB1" "$FB2" "$FB3" "$FB4" "$FB5" "$FB6")
|
||||
CLEANUP_NAMES+=( "${FB_ALL[@]}" )
|
||||
|
||||
info "5.1 CREATE + UPLOAD 6 функций параллельно (3 Python / 3 Node.js)..."
|
||||
|
||||
(set +e; create_py_deploy "$FB1" 128 '{"GRP_VAR":"py1"}') &
|
||||
(set +e; create_py_deploy "$FB2" 192 '{"GRP_VAR":"py2"}') &
|
||||
(set +e; create_py_deploy "$FB3" 256 '{"GRP_VAR":"py3"}') &
|
||||
(set +e; create_node_deploy "$FB4" 128 '{"GRP_VAR":"nd1"}') &
|
||||
(set +e; create_node_deploy "$FB5" 192 '{"GRP_VAR":"nd2"}') &
|
||||
(set +e; create_node_deploy "$FB6" 256 '{"GRP_VAR":"nd3"}') &
|
||||
wait
|
||||
|
||||
info "5.2 Проверяем что все 6 CRD созданы..."
|
||||
all_created=true
|
||||
for name in "${FB_ALL[@]}"; do
|
||||
ph=$(svc_phase "$name")
|
||||
if [[ -z "$ph" ]]; then
|
||||
fail "CRD $name не существует (create/upload провалился)"
|
||||
all_created=false
|
||||
fi
|
||||
done
|
||||
$all_created && pass "все 6 CRD созданы"
|
||||
|
||||
info "5.3 Ждём ALL READY (450s для 6 параллельных builds)..."
|
||||
not_ready_count=$(wait_all_ready 450 "${FB_ALL[@]}")
|
||||
if [[ "$not_ready_count" -eq 0 ]]; then
|
||||
pass "все 6 функций достигли Ready ✓"
|
||||
else
|
||||
fail "$not_ready_count из 6 НЕ достигли Ready за 450s"
|
||||
fi
|
||||
|
||||
# Ждём rollout всех Deployment
|
||||
info "5.4 Ждём rollout всех 6 Deployment (120s)..."
|
||||
all_rollout=true
|
||||
for name in "${FB_ALL[@]}"; do
|
||||
if ! kubectl rollout status deployment/"$name" -n "$DEPLOY_NS" --timeout=120s \
|
||||
> /dev/null 2>&1; then
|
||||
warn "$name rollout не завершился"
|
||||
all_rollout=false
|
||||
fi
|
||||
done
|
||||
sleep 5
|
||||
$all_rollout && pass "все 6 Deployment rollout завершён"
|
||||
|
||||
info "5.5 Python-функции: invoke × 3 к каждой, проверяем ok=True..."
|
||||
py_ok=true
|
||||
for name in "$FB1" "$FB2" "$FB3"; do
|
||||
for _i in 1 2 3; do
|
||||
rv=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||||
-d '{"msg":"flood"}' "$FN_BASE/$name")
|
||||
ok_val=$(echo "$rv" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('ok',''))" 2>/dev/null)
|
||||
if [[ "$ok_val" != "True" ]]; then
|
||||
fail "$name invoke[$_i] → ok='$ok_val' (resp: ${rv:0:100})"
|
||||
py_ok=false; break
|
||||
fi
|
||||
done
|
||||
done
|
||||
$py_ok && pass "все Python-функции invoke → ok=True"
|
||||
|
||||
info "5.6 Node.js-функции: invoke × 3 к каждой, проверяем ok=true..."
|
||||
nd_ok=true
|
||||
for name in "$FB4" "$FB5" "$FB6"; do
|
||||
for _i in 1 2 3; do
|
||||
rv=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||||
-d '{"msg":"flood"}' "$FN_BASE/$name")
|
||||
ok_val=$(echo "$rv" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(str(d.get('ok','')).lower())" 2>/dev/null)
|
||||
if [[ "$ok_val" != "true" ]]; then
|
||||
fail "$name invoke[$_i] → ok='$ok_val' (resp: ${rv:0:100})"
|
||||
nd_ok=false; break
|
||||
fi
|
||||
done
|
||||
done
|
||||
$nd_ok && pass "все Node.js-функции invoke → ok=true"
|
||||
|
||||
info "5.7 GRP_VAR env vars различаются у каждой функции..."
|
||||
env_ok=true
|
||||
declare -A EXPECTED_GRP
|
||||
EXPECTED_GRP["$FB1"]="py1"; EXPECTED_GRP["$FB2"]="py2"; EXPECTED_GRP["$FB3"]="py3"
|
||||
EXPECTED_GRP["$FB4"]="nd1"; EXPECTED_GRP["$FB5"]="nd2"; EXPECTED_GRP["$FB6"]="nd3"
|
||||
for name in "${FB_ALL[@]}"; do
|
||||
rv=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||||
-d '{}' "$FN_BASE/$name")
|
||||
got_grp=$(echo "$rv" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('grp',''))" 2>/dev/null)
|
||||
want="${EXPECTED_GRP[$name]}"
|
||||
if [[ "$got_grp" != "$want" ]]; then
|
||||
fail "$name grp='$got_grp' (ожидали '$want')"
|
||||
env_ok=false
|
||||
fi
|
||||
done
|
||||
$env_ok && pass "GRP_VAR корректен у всех 6 функций"
|
||||
|
||||
info "5.8 memory_mb применён в k8s Deployments..."
|
||||
mem_ok=true
|
||||
declare -A EXPECTED_MEM
|
||||
EXPECTED_MEM["$FB1"]="128Mi"; EXPECTED_MEM["$FB2"]="192Mi"; EXPECTED_MEM["$FB3"]="256Mi"
|
||||
EXPECTED_MEM["$FB4"]="128Mi"; EXPECTED_MEM["$FB5"]="192Mi"; EXPECTED_MEM["$FB6"]="256Mi"
|
||||
for name in "${FB_ALL[@]}"; do
|
||||
got_mem=$(kubectl get deployment "$name" -n "$DEPLOY_NS" \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].resources.limits.memory}' 2>/dev/null)
|
||||
want="${EXPECTED_MEM[$name]}"
|
||||
if [[ "$got_mem" != "$want" ]]; then
|
||||
fail "$name k8s memory='$got_mem' (ожидали '$want')"
|
||||
mem_ok=false
|
||||
fi
|
||||
done
|
||||
$mem_ok && pass "k8s memory limits корректны у всех 6 функций"
|
||||
|
||||
info "5.9 Удаляем все 6 flood-функций параллельно..."
|
||||
del_ok=true
|
||||
for name in "${FB_ALL[@]}"; do
|
||||
(api_code DELETE "$API/services/$name" > /dev/null 2>&1) &
|
||||
done
|
||||
wait
|
||||
sleep 5
|
||||
for name in "${FB_ALL[@]}"; do
|
||||
code=$(api_code GET "$API/services/$name")
|
||||
[[ "$code" == "404" ]] || { fail "$name: GET после DELETE → $code (ожидали 404)"; del_ok=false; }
|
||||
done
|
||||
$del_ok && pass "все 6 функций удалены, GET → 404"
|
||||
# Убираем из cleanup (уже удалены)
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]}" )
|
||||
for name in "${FB_ALL[@]}"; do
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$name}" )
|
||||
done
|
||||
|
||||
# Ждём очистки k8s
|
||||
sleep 15
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section 6 "RAPID DISCARD STORM — 30 create→DELETE без build"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: убедиться что 30 быстрых create+delete не оставляют orphan CRD.
|
||||
# Кейс: пользователь создал функцию, сразу передумал и удалил без upload.
|
||||
|
||||
info "6.1 Создаём + сразу удаляем 30 функций (10 параллельно, 3 волны)..."
|
||||
rd_creates_ok=0
|
||||
rd_deletes_ok=0
|
||||
rd_total=30
|
||||
|
||||
results_dir_6=$(mktemp -d)
|
||||
overflow=0
|
||||
for wave in 1 2 3; do
|
||||
wave_pids=()
|
||||
for slot in $(seq 1 10); do
|
||||
idx=$(( (wave - 1) * 10 + slot ))
|
||||
(
|
||||
set +e
|
||||
rname="rd${idx}-${SFX}"
|
||||
# CREATE
|
||||
http_c=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$rname\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"h.h\",\"memory_mb\":128,\"env_vars\":{}}")
|
||||
# Немедленно DELETE — не ждём Building
|
||||
http_d=$(api_code DELETE "$API/services/$rname")
|
||||
echo "${http_c}:${http_d}" > "$results_dir_6/${idx}"
|
||||
) &
|
||||
wave_pids+=($!)
|
||||
done
|
||||
wait "${wave_pids[@]}"
|
||||
done
|
||||
|
||||
# Подсчёт результатов
|
||||
for file in "$results_dir_6"/*; do
|
||||
pair=$(cat "$file")
|
||||
c_code="${pair%%:*}"
|
||||
d_code="${pair##*:}"
|
||||
[[ "$c_code" == "201" ]] && rd_creates_ok=$((rd_creates_ok + 1))
|
||||
[[ "$d_code" == "204" || "$d_code" == "404" ]] && rd_deletes_ok=$((rd_deletes_ok + 1))
|
||||
done
|
||||
rm -rf "$results_dir_6"
|
||||
|
||||
if [[ $rd_creates_ok -ge 27 ]]; then # допустим ≤3 ошибки (гонка)
|
||||
pass "$rd_creates_ok/$rd_total create вернули 201"
|
||||
else
|
||||
fail "$rd_creates_ok/$rd_total create вернули 201 (ожидали ≥27)"
|
||||
fi
|
||||
if [[ $rd_deletes_ok -ge 27 ]]; then
|
||||
pass "$rd_deletes_ok/$rd_total delete вернули 204/404"
|
||||
else
|
||||
fail "$rd_deletes_ok/$rd_total delete ок (ожидали ≥27)"
|
||||
fi
|
||||
|
||||
info "6.2 Ждём 20s чтобы finalizer-ы отработали..."
|
||||
sleep 20
|
||||
|
||||
info "6.3 Проверяем что нет orphan CRD в namespace $NS..."
|
||||
orphan_count=$(api_json GET "$API/services" \
|
||||
| python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
items = data if isinstance(data, list) else data.get('items', data.get('services', []))
|
||||
surv = [i.get('name','') for i in items if '${SFX}' in i.get('name','')]
|
||||
print(len(surv))
|
||||
print('\n'.join(surv))
|
||||
" 2>/dev/null | head -1)
|
||||
if [[ "$orphan_count" -eq 0 ]]; then
|
||||
pass "нет orphan CRD — все $rd_total функций удалены чисто"
|
||||
else
|
||||
fail "$orphan_count orphan CRD обнаружено после rapid discard"
|
||||
fi
|
||||
|
||||
info "6.4 Нет orphan Deployment/Service в deploy namespace $DEPLOY_NS..."
|
||||
orphan_k8s=$(kubectl get deployment -n "$DEPLOY_NS" --no-headers 2>/dev/null \
|
||||
| grep "${SFX}" | wc -l)
|
||||
if [[ "$orphan_k8s" -eq 0 ]]; then
|
||||
pass "нет orphan Deployment в $DEPLOY_NS"
|
||||
else
|
||||
fail "$orphan_k8s orphan Deployment в $DEPLOY_NS после rapid discard"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section 7 "CHURN UNDER FIRE — 10 PUT env-updates под 200 параллельными invokes"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: обновления конфигурации не ломают работу функции под нагрузкой.
|
||||
# Метрика: success rate invoke >= 90%, финальный env корректный.
|
||||
|
||||
CU="churn-${SFX}"
|
||||
CLEANUP_NAMES+=("$CU")
|
||||
|
||||
info "7.1 Создаём и деплоим churn-функцию..."
|
||||
if create_py_deploy "$CU" 192 '{"TEST_VAR":"ver-0","GRP_VAR":"churn"}'; then
|
||||
pass "create+upload $CU OK"
|
||||
else
|
||||
fail "create+upload $CU провалился, пропускаем группу 7"
|
||||
# Очистка + переход к следующей группе
|
||||
cleanup "$CU"
|
||||
fi
|
||||
|
||||
info "7.2 Ждём Ready (300s)..."
|
||||
cu_ready=0
|
||||
wait_phase "$CU" "Ready" 300 || cu_ready=$?
|
||||
if [[ $cu_ready -eq 0 ]]; then
|
||||
pass "$CU Ready"
|
||||
kubectl rollout status deployment/"$CU" -n "$DEPLOY_NS" --timeout=60s \
|
||||
> /dev/null 2>&1 || true
|
||||
sleep 3
|
||||
else
|
||||
fail "$CU не вышел в Ready (фаза: $(svc_phase "$CU")), пропускаем тест"
|
||||
cleanup "$CU"; CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$CU}" )
|
||||
# jump over group 7 остальные тесты пропустим
|
||||
cu_ready=99
|
||||
fi
|
||||
|
||||
if [[ $cu_ready -eq 0 ]]; then
|
||||
info "7.3 Запускаем фоновый поток: 200 invoke (10 параллельно × 20 волн)..."
|
||||
# Invoke в фоне — пишем коды в файлы
|
||||
churn_results=$(mktemp -d)
|
||||
(
|
||||
for wave in $(seq 1 20); do
|
||||
wave_pids=()
|
||||
for _b in $(seq 1 10); do
|
||||
idx=$(( (wave - 1) * 10 + _b ))
|
||||
(
|
||||
code=$(invoke_one "$CU")
|
||||
echo "$code" > "$churn_results/${idx}"
|
||||
) &
|
||||
wave_pids+=($!)
|
||||
done
|
||||
wait "${wave_pids[@]}"
|
||||
sleep 0.5 # небольшая пауза между волнами чтобы не throttle
|
||||
done
|
||||
) &
|
||||
fire_pid=$!
|
||||
|
||||
info "7.4 Пока идут invoke — делаем 10 PUT с нарастающим TEST_VAR..."
|
||||
for ver in $(seq 1 10); do
|
||||
sleep 4 # ~каждые 4s новый PUT
|
||||
http_put=$(api_code PUT "$API/services/$CU" \
|
||||
"{\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":192,\"env_vars\":{\"TEST_VAR\":\"ver-${ver}\",\"GRP_VAR\":\"churn\"}}")
|
||||
if [[ "$http_put" == "200" ]]; then
|
||||
info " PUT ver-${ver} → 200 ✓"
|
||||
else
|
||||
fail "PUT ver-${ver} → HTTP $http_put"
|
||||
fi
|
||||
done
|
||||
|
||||
info "7.5 Ждём завершения фонового invoke-потока..."
|
||||
wait $fire_pid
|
||||
|
||||
# Подсчёт invoke результатов
|
||||
cu_success=0; cu_fail_cnt=0
|
||||
for file in "$churn_results"/*; do
|
||||
code=$(cat "$file")
|
||||
if [[ "$code" == "200" ]]; then cu_success=$((cu_success + 1))
|
||||
else cu_fail_cnt=$((cu_fail_cnt + 1)); fi
|
||||
done
|
||||
rm -rf "$churn_results"
|
||||
total_invokes=$((cu_success + cu_fail_cnt))
|
||||
success_pct=0
|
||||
[[ $total_invokes -gt 0 ]] && success_pct=$(( cu_success * 100 / total_invokes ))
|
||||
info " Invoke сводка: $cu_success/$total_invokes успешных ($success_pct%)"
|
||||
|
||||
if [[ $success_pct -ge 90 ]]; then
|
||||
pass "invoke success rate $success_pct% ≥ 90% под 10 rolling updates"
|
||||
else
|
||||
fail "invoke success rate $success_pct% < 90% (много гонок при update?)"
|
||||
fi
|
||||
|
||||
info "7.6 Ждём rollout финального обновления (ver-10)..."
|
||||
sleep 10
|
||||
kubectl rollout status deployment/"$CU" -n "$DEPLOY_NS" --timeout=90s \
|
||||
> /dev/null 2>&1 || true
|
||||
sleep 5
|
||||
|
||||
info "7.7 Финальный invoke → TEST_VAR должен быть ver-10..."
|
||||
final_resp=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||||
-d '{}' "$FN_BASE/$CU")
|
||||
final_env=$(echo "$final_resp" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('env',''))" 2>/dev/null)
|
||||
if [[ "$final_env" == "ver-10" ]]; then
|
||||
pass "финальный env=ver-10 ✓"
|
||||
else
|
||||
fail "финальный env='$final_env' (ожидали 'ver-10')"
|
||||
fi
|
||||
|
||||
info "7.8 k8s Deployment env = ver-10..."
|
||||
k8s_env_cu=$(kubectl get deployment "$CU" -n "$DEPLOY_NS" -o json 2>/dev/null \
|
||||
| python3 -c "import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
envs=d['spec']['template']['spec']['containers'][0].get('env',[])
|
||||
val=next((e['value'] for e in envs if e['name']=='TEST_VAR'), '')
|
||||
print(val)" 2>/dev/null)
|
||||
if [[ "$k8s_env_cu" == "ver-10" ]]; then
|
||||
pass "k8s Deployment TEST_VAR=ver-10 ✓"
|
||||
else
|
||||
fail "k8s Deployment TEST_VAR='$k8s_env_cu' (ожидали 'ver-10')"
|
||||
fi
|
||||
fi
|
||||
|
||||
info "7.9 Удаляем churn-функцию..."
|
||||
check_code "DELETE $CU" "$(api_code DELETE "$API/services/$CU")" "204"
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$CU}" )
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section 8 "PHOENIX — 3 цикла DELETE + recreate одного имени"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: после DELETE сервиса его имя можно переиспользовать немедленно.
|
||||
# Финалайзер должен корректно удалить k8s ресурсы прежде чем CRD исчезнет.
|
||||
# Каждый цикл: create → wait Ready → DELETE → wait 404 → снова create.
|
||||
|
||||
PX="phoenix-${SFX}"
|
||||
CLEANUP_NAMES+=("$PX")
|
||||
|
||||
phoenix_all_ok=true
|
||||
for cycle in 1 2 3; do
|
||||
info "8.${cycle} PHOENIX цикл $cycle/3..."
|
||||
|
||||
# CREATE
|
||||
cx_code=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$PX\",\"display_name\":\"phoenix c$cycle\",\
|
||||
\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||||
\"memory_mb\":128,\"env_vars\":{\"TEST_VAR\":\"cycle-${cycle}\"}}")
|
||||
if [[ "$cx_code" != "201" ]]; then
|
||||
fail " phoenix цикл $cycle: CREATE → HTTP $cx_code (ожидали 201)"
|
||||
phoenix_all_ok=false; continue
|
||||
fi
|
||||
|
||||
# UPLOAD
|
||||
make_python_zip "${PX}_cycle${cycle}"
|
||||
cp "/tmp/surv_py_${PX}_cycle${cycle}.zip" "/tmp/surv_py_${PX}.zip" 2>/dev/null || true
|
||||
make_python_zip "${PX}"
|
||||
u_code=$(upload_zip "$PX" "/tmp/surv_py_${PX}.zip")
|
||||
if [[ "$u_code" != "200" ]]; then
|
||||
fail " phoenix цикл $cycle: UPLOAD → HTTP $u_code"
|
||||
phoenix_all_ok=false; cleanup "$PX"; continue
|
||||
fi
|
||||
|
||||
# WAIT READY
|
||||
px_w=0
|
||||
wait_phase "$PX" "Ready" 300 || px_w=$?
|
||||
if [[ $px_w -ne 0 ]]; then
|
||||
fail " phoenix цикл $cycle: не дошёл до Ready (w=$px_w, phase=$(svc_phase "$PX"))"
|
||||
phoenix_all_ok=false; cleanup "$PX"; continue
|
||||
fi
|
||||
kubectl rollout status deployment/"$PX" -n "$DEPLOY_NS" --timeout=60s \
|
||||
> /dev/null 2>&1 || true
|
||||
sleep 3
|
||||
|
||||
# INVOKE — убедиться что работает и env правильный
|
||||
px_resp=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||||
-d '{}' "$FN_BASE/$PX")
|
||||
px_env=$(echo "$px_resp" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('env',''))" 2>/dev/null)
|
||||
if [[ "$px_env" == "cycle-${cycle}" ]]; then
|
||||
pass " phoenix цикл $cycle: Ready + invoke env=cycle-${cycle} ✓"
|
||||
else
|
||||
fail " phoenix цикл $cycle: invoke env='$px_env' (ожидали 'cycle-${cycle}')"
|
||||
phoenix_all_ok=false
|
||||
fi
|
||||
|
||||
# DELETE
|
||||
del_code=$(api_code DELETE "$API/services/$PX")
|
||||
if [[ "$del_code" != "204" ]]; then
|
||||
fail " phoenix цикл $cycle: DELETE → HTTP $del_code"
|
||||
phoenix_all_ok=false; continue
|
||||
fi
|
||||
|
||||
# WAIT 404 — ждём когда finalizer уберёт CRD
|
||||
info " Ждём когда CRD исчезнет (finalizer, 60s)..."
|
||||
gone=false
|
||||
for _w in $(seq 1 12); do
|
||||
sleep 5
|
||||
code404=$(api_code GET "$API/services/$PX")
|
||||
if [[ "$code404" == "404" ]]; then gone=true; break; fi
|
||||
done
|
||||
if ! $gone; then
|
||||
fail " phoenix цикл $cycle: CRD $PX не исчез за 60s после DELETE"
|
||||
phoenix_all_ok=false
|
||||
fi
|
||||
|
||||
# Проверяем k8s Deployment тоже удалён
|
||||
k8s_gone=false
|
||||
for _w in $(seq 1 6); do
|
||||
sleep 5
|
||||
deploy_ns_has "$PX" || { k8s_gone=true; break; }
|
||||
done
|
||||
if ! $k8s_gone; then
|
||||
fail " phoenix цикл $cycle: k8s Deployment не удалён за 30s"
|
||||
phoenix_all_ok=false
|
||||
fi
|
||||
|
||||
info " Цикл $cycle завершён — имя $PX освободилось"
|
||||
done
|
||||
|
||||
if $phoenix_all_ok; then
|
||||
pass "PHOENIX: все 3 цикла DELETE+recreate прошли успешно"
|
||||
fi
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$PX}" )
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section 9 "SELF-HEALING MARATHON — 5 раз вручную убить Deployment"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: оператор с RequeueAfter:60s восстанавливает Deployment каждый раз.
|
||||
# Без сбоев подряд: удаляем → ждём 90s → убедиться что пересоздан → invoke OK.
|
||||
|
||||
SH="sheal-${SFX}"
|
||||
CLEANUP_NAMES+=("$SH")
|
||||
|
||||
info "9.1 Создаём и деплоим self-healing функцию..."
|
||||
if create_py_deploy "$SH" 128 '{"TEST_VAR":"sheal"}'; then
|
||||
sh_ready=0
|
||||
wait_phase "$SH" "Ready" 300 || sh_ready=$?
|
||||
if [[ $sh_ready -eq 0 ]]; then
|
||||
pass "$SH Ready перед self-healing marathon"
|
||||
kubectl rollout status deployment/"$SH" -n "$DEPLOY_NS" --timeout=60s \
|
||||
> /dev/null 2>&1 || true
|
||||
sleep 3
|
||||
else
|
||||
fail "$SH не вышел в Ready, пропускаем группу 9"
|
||||
sh_ready=99
|
||||
fi
|
||||
else
|
||||
fail "create/upload $SH провалился, пропускаем группу 9"
|
||||
sh_ready=99
|
||||
fi
|
||||
|
||||
if [[ "${sh_ready:-0}" -eq 0 ]]; then
|
||||
sheal_ok=0
|
||||
for kill_round in 1 2 3 4 5; do
|
||||
info "9.kill$kill_round — убиваем Deployment (раунд $kill_round/5)..."
|
||||
kubectl delete deployment "$SH" -n "$DEPLOY_NS" > /dev/null 2>&1
|
||||
|
||||
info " Ждём пересоздания (90s = минимум один RequeueAfter: 60s цикл + delta)..."
|
||||
recreated=false
|
||||
for _w in $(seq 1 18); do
|
||||
sleep 5
|
||||
if deploy_ns_has "$SH"; then recreated=true; break; fi
|
||||
done
|
||||
|
||||
if $recreated; then
|
||||
# Ждём pod готовности
|
||||
kubectl rollout status deployment/"$SH" -n "$DEPLOY_NS" --timeout=90s \
|
||||
> /dev/null 2>&1 || true
|
||||
sleep 3
|
||||
# Invoke должен работать
|
||||
sh_resp=$(curl -s -m 30 -X POST -H "Content-Type: application/json" \
|
||||
-d '{"iter":'"$kill_round"'}' "$FN_BASE/$SH")
|
||||
sh_ok=$(echo "$sh_resp" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print(d.get('ok',''))" 2>/dev/null)
|
||||
if [[ "$sh_ok" == "True" ]]; then
|
||||
pass " раунд $kill_round: Deployment пересоздан + invoke OK"
|
||||
sheal_ok=$((sheal_ok + 1))
|
||||
else
|
||||
fail " раунд $kill_round: Deployment пересоздан, но invoke → ${sh_resp:0:80}"
|
||||
fi
|
||||
else
|
||||
fail " раунд $kill_round: Deployment НЕ пересоздан за 90s после удаления"
|
||||
fi
|
||||
done
|
||||
info "Self-healing итог: $sheal_ok/5 раундов прошли"
|
||||
fi
|
||||
|
||||
info "9.final Удаляем $SH..."
|
||||
check_code "DELETE $SH" "$(api_code DELETE "$API/services/$SH")" "204"
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$SH}" )
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section 10 "INVOKE HURRICANE — 500 параллельных invokes в 5 волнах"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: функция держит нагрузку 100 одновременных запросов × 5 волн = 500 total.
|
||||
# Метрика: success rate >= 90%, нет зависаний.
|
||||
|
||||
HU="hurr-${SFX}"
|
||||
CLEANUP_NAMES+=("$HU")
|
||||
|
||||
info "10.1 Создаём hurricane-функцию (Node.js, 256Mi)..."
|
||||
if create_node_deploy "$HU" 256 '{"TEST_VAR":"hurricane","GRP_VAR":"load"}'; then
|
||||
hu_ready=0
|
||||
wait_phase "$HU" "Ready" 300 || hu_ready=$?
|
||||
if [[ $hu_ready -eq 0 ]]; then
|
||||
pass "$HU Ready"
|
||||
kubectl rollout status deployment/"$HU" -n "$DEPLOY_NS" --timeout=90s \
|
||||
> /dev/null 2>&1 || true
|
||||
sleep 5
|
||||
else
|
||||
fail "$HU не вышел в Ready, пропускаем группу 10"
|
||||
hu_ready=99
|
||||
fi
|
||||
else
|
||||
fail "create/upload $HU провалился, пропускаем группу 10"
|
||||
hu_ready=99
|
||||
fi
|
||||
|
||||
if [[ "${hu_ready:-0}" -eq 0 ]]; then
|
||||
info "10.2 Прогрев: 10 последовательных invoke перед нагрузкой..."
|
||||
warmup_ok=0
|
||||
for _w in $(seq 1 10); do
|
||||
code=$(invoke_one "$HU")
|
||||
[[ "$code" == "200" ]] && warmup_ok=$((warmup_ok + 1))
|
||||
done
|
||||
if [[ $warmup_ok -ge 8 ]]; then
|
||||
pass "прогрев: $warmup_ok/10 OK"
|
||||
else
|
||||
fail "прогрев: $warmup_ok/10 OK (функция не стабильна)"
|
||||
fi
|
||||
|
||||
info "10.3 ВОЛНА 1-5: 100 параллельных запросов в каждой волне (500 total)..."
|
||||
total_ok=0; total_fail=0
|
||||
for wave in 1 2 3 4 5; do
|
||||
info " ВОЛНА $wave/5 (100 параллельных)..."
|
||||
# Запускаем 100 параллельно
|
||||
wave_dir=$(mktemp -d)
|
||||
wave_pids=()
|
||||
for req in $(seq 1 100); do
|
||||
(
|
||||
code=$(invoke_one "$HU")
|
||||
echo "$code" > "$wave_dir/${req}"
|
||||
) &
|
||||
wave_pids+=($!)
|
||||
done
|
||||
wait "${wave_pids[@]}"
|
||||
|
||||
wave_ok=0; wave_fail=0
|
||||
for file in "$wave_dir"/*; do
|
||||
c=$(cat "$file")
|
||||
if [[ "$c" == "200" ]]; then wave_ok=$((wave_ok + 1))
|
||||
else wave_fail=$((wave_fail + 1)); fi
|
||||
done
|
||||
rm -rf "$wave_dir"
|
||||
total_ok=$((total_ok + wave_ok))
|
||||
total_fail=$((total_fail + wave_fail))
|
||||
info " волна $wave: $wave_ok/100 OK, $wave_fail FAIL"
|
||||
sleep 2 # небольшой перерыв между волнами
|
||||
done
|
||||
|
||||
total=$((total_ok + total_fail))
|
||||
pct=0
|
||||
[[ $total -gt 0 ]] && pct=$(( total_ok * 100 / total ))
|
||||
info " Итого: $total_ok/$total OK ($pct%)"
|
||||
|
||||
if [[ $pct -ge 90 ]]; then
|
||||
pass "HURRICANE: $total_ok/$total invoke → ${pct}% success rate (≥90%) ✓"
|
||||
else
|
||||
fail "HURRICANE: $total_ok/$total invoke → ${pct}% success rate (<90%)"
|
||||
fi
|
||||
|
||||
info "10.4 Функция стабильна после шторма — финальный invoke..."
|
||||
sleep 5
|
||||
final_code=$(invoke_one "$HU")
|
||||
if [[ "$final_code" == "200" ]]; then
|
||||
pass "финальный invoke после нагрузки → 200 ✓"
|
||||
else
|
||||
fail "финальный invoke после нагрузки → $final_code"
|
||||
fi
|
||||
fi
|
||||
|
||||
info "10.5 Удаляем hurricane-функцию..."
|
||||
check_code "DELETE $HU" "$(api_code DELETE "$API/services/$HU")" "204"
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$HU}" )
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||||
echo -e "${BOLD}║ ИТОГИ SURVIVAL ║${RESET}"
|
||||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||||
echo ""
|
||||
echo " Время завершения : $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo ""
|
||||
echo " Всего тестов : $((PASS + FAIL))"
|
||||
echo " PASS : $PASS"
|
||||
echo " FAIL : $FAIL"
|
||||
echo ""
|
||||
|
||||
# Per-group итоги
|
||||
for grp in G5 G6 G7 G8 G9 G10; do
|
||||
g_pass=${GRP_PASS[$grp]:-0}
|
||||
g_fail=${GRP_FAIL[$grp]:-0}
|
||||
g_total=$((g_pass + g_fail))
|
||||
label=""
|
||||
case "$grp" in
|
||||
G5) label="FLOOD BUILD";; G6) label="RAPID DISCARD STORM";;
|
||||
G7) label="CHURN UNDER FIRE";; G8) label="PHOENIX";;
|
||||
G9) label="SELF-HEALING MARATHON";; G10) label="INVOKE HURRICANE";;
|
||||
esac
|
||||
if [[ $g_fail -eq 0 ]]; then
|
||||
echo -e " ${GREEN}✓${RESET} Группа $grp ($label): ${g_pass}/${g_total}"
|
||||
else
|
||||
echo -e " ${RED}✗${RESET} Группа $grp ($label): ${g_pass}/${g_total} (FAIL: $g_fail)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [[ $FAIL -eq 0 ]]; then
|
||||
echo -e " ${GREEN}${BOLD}✓ ВЫЖИЛ — ВСЕ ТЕСТЫ ПРОШЛИ${RESET}"
|
||||
else
|
||||
echo -e " ${RED}${BOLD}✗ ПРОВАЛ — $FAIL тестов упало${RESET}"
|
||||
fi
|
||||
echo ""
|
||||
Reference in New Issue
Block a user