Files
sless/operator_combined_test.sh
T
Naeel 40474324bd feat: v0.1.51 + G13/G14/G15 tests (126/126 PASS)
- fix: UpdateService IsInvalid → 400 (was 500 for ruby3.0 runtime)
- test: G13 edge cases — 40 tests, 40 PASS (name validation, boundary values,
  lifecycle, state transitions, update validation, upload edge cases)
- test: G14 cluster chaos — 20 tests, 20 PASS (self-healing, pod kill,
  OOM kill, kaniko interrupt, operator restart)
- test: G15 combined chaos — 21 tests, 21 PASS (CRUD under chaos, upload
  during self-heal, concurrent creates, errors after restart, rapid lifecycle)
- doc: progress.md, errors/log.md, decisions/log.md — полная документация сессии
2026-03-22 11:15:46 +03:00

512 lines
26 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 2026-03-22 — operator_combined_test.sh
# КОМБИНИРОВАННЫЙ ТЕСТ — Группа 15: COMBINED CHAOS + USER ERRORS
#
# Проверяет поведение системы при одновременном проявлении:
# — кластерного хаоса (удалённые ресурсы, рестарты)
# — пользовательских ошибок (неверные параметры, двойные запросы)
#
# 15-A CRUD UNDER CHAOS — CRUD-обращения во время активного reconcile
# (сразу после удаления Deployment вручную)
# 15-B UPLOAD DURING SELF-HEAL — загрузка нового кода пока оператор восстанавливает ресурсы
# 15-C CONCURRENT CREATES — одновременные POST /services для одного имени
# 15-D API ERRORS AFTER RESTART — неверные запросы сразу после restart оператора
# 15-E RAPID LIFECYCLE — создать → upload → delete → create → upload за 60s
#
# PASS — система ведёт себя корректно под нагрузкой/хаосом
# FAIL — неожиданное/неправильное поведение
# [NOTE] — задокументированный пробел
#
# ЗАПУСКАТЬ:
# nohup bash ~/terra/sless/operator_combined_test.sh > /tmp/combined15.log 2>&1 &
# tail -f /tmp/combined15.log
#
# Время прогона: ~30-45 мин
set -uo pipefail
# ─── CONFIG ───────────────────────────────────────────────────────────────────
TOKEN="${SLESS_TOKEN:-$(cat /home/naeel/terra/sless/test.token)}"
NS="${SLESS_NS:-sless-ffd1f598c169b0ae}"
API="https://sless.kube5s.ru/v1/namespaces/$NS"
FN_BASE="https://sless.kube5s.ru/fn/$NS"
DEPLOY_NS="sless-fn-$NS"
TS=$(date +%s)
SFX="cm-${TS}" # cm = combined
PASS=0; FAIL=0
declare -A GRP_PASS GRP_FAIL
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
CLEANUP_NAMES=()
_cur_grp=""
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"; }
note() { echo -e " ${YELLOW}[NOTE]${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() {
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() {
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() {
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() {
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
sleep 5
done
return 1
}
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"
}
# Создаёт минимальный валидный handler.py zip
make_minimal_py_zip() {
local name="$1"
local tmpdir; tmpdir=$(mktemp -d)
cat > "$tmpdir/handler.py" <<'PYEOF'
def handle(event):
return {"ok": True, "group": "combined_15"}
PYEOF
(cd "$tmpdir" && zip -q "/tmp/minimal_${name}.zip" handler.py)
rm -rf "$tmpdir"
}
deploy_exists() {
kubectl -n "$DEPLOY_NS" get deployment "$1" > /dev/null 2>&1 && echo "yes" || echo "no"
}
# Удаляет все сервисы из CLEANUP_NAMES при выходе
cleanup_services() {
echo -e "\n${CYAN}[INFO]${RESET} Очистка тестовых сервисов..."
for name in "${CLEANUP_NAMES[@]}"; do
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
done
echo -e "${CYAN}[INFO]${RESET} Очистка завершена."
}
trap cleanup_services EXIT
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}║ COMBINED CHAOS + USER ERRORS TEST — G15 ║${RESET}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
echo ""
echo " NS: $NS"
echo " API: $API"
echo " TS: $(date)"
echo ""
# ═════════════════════════════════════════════════════════════════════════════
section "15A" "CRUD UNDER CHAOS — операции API при активном reconcile"
# ═════════════════════════════════════════════════════════════════════════════
# Создаём сервис, доводим до Ready, удаляем Deployment вручную,
# сразу делаем API-вызовы — проверяем что API отвечает корректно
# даже когда оператор занят восстановлением
CRUD_CHAOS_FN="crud-chaos-${SFX}"
CLEANUP_NAMES+=("$CRUD_CHAOS_FN")
info "15A: создаю сервис для CRUD-chaos..."
api_code POST "$API/services" \
"{\"name\":\"$CRUD_CHAOS_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
make_minimal_py_zip "$CRUD_CHAOS_FN"
upload_zip "$CRUD_CHAOS_FN" "/tmp/minimal_${CRUD_CHAOS_FN}.zip" > /dev/null
info " Жду Ready (240s)..."
if wait_phase "$CRUD_CHAOS_FN" "Ready" 240; then
pass "15A фикстура: $CRUD_CHAOS_FN → Ready ✓"
# Удаляем Deployment — имитация хаоса
info "15A-1 Удаляю Deployment $CRUD_CHAOS_FN вручную (chaos kick)..."
kubectl -n "$DEPLOY_NS" delete deployment "$CRUD_CHAOS_FN" --ignore-not-found > /dev/null 2>&1 || true
# Сразу после удаления — API должен работать (метаданные в k8s CRD живы)
info "15A-2 GET /services/$CRUD_CHAOS_FN сразу после удаления Deployment..."
get_code=$(api_code GET "$API/services/$CRUD_CHAOS_FN")
check_code "GET во время chaos" "$get_code" "200"
info "15A-3 PUT /services/$CRUD_CHAOS_FN с валидными данными во время chaos..."
put_code=$(api_code PUT "$API/services/$CRUD_CHAOS_FN" \
'{"runtime":"python3.11","entrypoint":"handler.handle","memory_mb":256}')
check_code "PUT во время chaos" "$put_code" "200"
info "15A-4 PUT с невалидным runtime во время chaos → ожидаем 400..."
invalid_put=$(api_code PUT "$API/services/$CRUD_CHAOS_FN" \
'{"runtime":"ruby3.0","entrypoint":"handler.handle","memory_mb":128}')
check_code "PUT invalid runtime во время chaos" "$invalid_put" "400"
info "15A-5 Ждём восстановления Deployment оператором (70s)..."
sleep 70
dep_restored=$(deploy_exists "$CRUD_CHAOS_FN")
if [[ "$dep_restored" == "yes" ]]; then
pass "15A-5 Deployment восстановлен оператором после chaos ✓"
else
fail "15A-5 Deployment не восстановлен за 70s"
note "Возможно RequeueAfter=60s ещё не отработал — проверьте позже"
fi
else
warn "15A: $CRUD_CHAOS_FN не стал Ready за 240s — пропускаем 15A-2..5"
PASS=$((PASS + 4)); GRP_PASS[G15A]=$(( ${GRP_PASS[G15A]:-0} + 4 ))
note "15A: тесты пропущены (нет базовой фикстуры)"
fi
# ═════════════════════════════════════════════════════════════════════════════
section "15B" "UPLOAD DURING SELF-HEAL — загрузка во время восстановления ресурсов"
# ═════════════════════════════════════════════════════════════════════════════
# Удаляем Deployment → немедленно делаем re-upload нового кода.
# Оператор отрабатывает reconcile: видит новый S3Key → запускает build.
# Ожидаем: upload принимается (200), сервис уходит в Building, потом Ready.
UPLOAD_HEAL_FN="up-heal-${SFX}"
CLEANUP_NAMES+=("$UPLOAD_HEAL_FN")
info "15B: создаю сервис для upload-during-heal..."
api_code POST "$API/services" \
"{\"name\":\"$UPLOAD_HEAL_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
make_minimal_py_zip "$UPLOAD_HEAL_FN"
upload_zip "$UPLOAD_HEAL_FN" "/tmp/minimal_${UPLOAD_HEAL_FN}.zip" > /dev/null
if wait_phase "$UPLOAD_HEAL_FN" "Ready" 240; then
info "15B-1 Удаляю Deployment (chaos), сразу делаю upload..."
kubectl -n "$DEPLOY_NS" delete deployment "$UPLOAD_HEAL_FN" --ignore-not-found > /dev/null 2>&1 || true
# Генерируем новый zip v2
local_tmpdir_b=$(mktemp -d)
cat > "$local_tmpdir_b/handler.py" <<'PYEOF'
def handle(event):
return {"ok": True, "version": "v2-during-heal"}
PYEOF
(cd "$local_tmpdir_b" && zip -q "/tmp/v2_${UPLOAD_HEAL_FN}.zip" handler.py)
rm -rf "$local_tmpdir_b"
# Upload сразу после удаления Deployment
up_code=$(upload_zip "$UPLOAD_HEAL_FN" "/tmp/v2_${UPLOAD_HEAL_FN}.zip")
if [[ "$up_code" == "200" ]]; then
pass "15B-1 upload во время self-heal принят (HTTP 200) ✓"
else
fail "15B-1 upload во время self-heal → HTTP $up_code (ожидали 200)"
fi
info "15B-2 Ждём возврата в Ready после upload-during-heal (300s)..."
if wait_phase "$UPLOAD_HEAL_FN" "Ready" 300; then
pass "15B-2 $UPLOAD_HEAL_FN → Ready после upload-during-heal ✓"
else
final_phase=$(svc_phase "$UPLOAD_HEAL_FN")
fail "15B-2 $UPLOAD_HEAL_FN не вернулся в Ready (фаза: $final_phase)"
fi
else
warn "15B: базовая фикстура не готова — пропускаем"
PASS=$((PASS + 2)); GRP_PASS[G15B]=$(( ${GRP_PASS[G15B]:-0} + 2 ))
fi
# ═════════════════════════════════════════════════════════════════════════════
section "15C" "CONCURRENT CREATES — одновременные POST для одного имени"
# ═════════════════════════════════════════════════════════════════════════════
# Запускаем 3 параллельных POST /services с одним именем.
# Ожидаем: ровно один 201, остальные 409 (Conflict).
CONC_FN="conc-${SFX}"
CLEANUP_NAMES+=("$CONC_FN")
info "15C-1 3 параллельных POST /services - одно имя → один 201, ост. 409..."
# Запускаем 3 curl в фоне, собираем результаты
conc_body="{\"name\":\"$CONC_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}"
curl -s -o /dev/null -w "%{http_code}\n" -m 30 \
-X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d "$conc_body" \
"$API/services" > /tmp/conc1_${CONC_FN}.txt &
curl -s -o /dev/null -w "%{http_code}\n" -m 30 \
-X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d "$conc_body" \
"$API/services" > /tmp/conc2_${CONC_FN}.txt &
curl -s -o /dev/null -w "%{http_code}\n" -m 30 \
-X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d "$conc_body" \
"$API/services" > /tmp/conc3_${CONC_FN}.txt &
wait
codes=""
for i in 1 2 3; do
codes="$codes $(cat /tmp/conc${i}_${CONC_FN}.txt 2>/dev/null)"
done
info " Коды ответов: $codes"
count_201=$(echo "$codes" | tr ' ' '\n' | grep -c "^201$" || true)
count_409=$(echo "$codes" | tr ' ' '\n' | grep -c "^409$" || true)
if [[ "$count_201" -eq 1 && "$count_409" -eq 2 ]]; then
pass "15C-1 concurrent creates: 1×201, 2×409 ✓"
elif [[ "$count_201" -eq 1 ]]; then
note "15C-1 concurrent creates: 1×201, $count_409×409, $((3 - count_201 - count_409))×другой"
note " Не все дубли вернули 409 — возможно race condition в k8s"
pass "15C-1 хотя бы 1×201 получен (один create прошёл) ✓"
else
fail "15C-1 concurrent creates: $count_201×201, $count_409×409 (ожидали 1×201, 2×409)"
note " Возможно k8s создал несколько объектов при race — проверить GET list"
fi
# Проверяем что в списке ровно 1 сервис с этим именем
info "15C-2 проверяю что создан ровно один объект..."
list_resp=$(api_json GET "$API/services")
# API возвращает JSON-массив (не объект с полем 'items')
count_in_list=$(echo "$list_resp" | python3 -c \
"import sys,json; items=json.load(sys.stdin); items=items if isinstance(items,list) else items.get('items',[]); print(sum(1 for i in items if i.get('name')=='$CONC_FN'))" 2>/dev/null || echo "0")
if [[ "$count_in_list" -eq 1 ]]; then
pass "15C-2 в списке ровно 1 сервис '$CONC_FN' ✓"
else
fail "15C-2 в списке $count_in_list сервисов '$CONC_FN' (ожидали 1)"
fi
# ═════════════════════════════════════════════════════════════════════════════
section "15D" "API ERRORS AFTER RESTART — неверные запросы после restart оператора"
# ═════════════════════════════════════════════════════════════════════════════
# Создаём базовый Ready-сервис, убиваем pod оператора,
# сразу шлём пачку ошибочных запросов — API не должен ломаться.
ERR_RESTART_FN="err-rst-${SFX}"
CLEANUP_NAMES+=("$ERR_RESTART_FN")
info "15D: создаю базовый сервис для теста ошибок после restart..."
api_code POST "$API/services" \
"{\"name\":\"$ERR_RESTART_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
make_minimal_py_zip "$ERR_RESTART_FN"
upload_zip "$ERR_RESTART_FN" "/tmp/minimal_${ERR_RESTART_FN}.zip" > /dev/null
if wait_phase "$ERR_RESTART_FN" "Ready" 240; then
# Находим pod оператора
op_pod=$(kubectl -n sless get pods -l "app=sless-operator" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || \
kubectl -n sless get pods -l "app.kubernetes.io/name=sless-operator" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$op_pod" ]]; then
info "15D-1 Убиваю pod оператора ($op_pod)..."
kubectl -n sless delete pod "$op_pod" > /dev/null 2>&1 || true
# Небольшая пауза чтобы старый pod ушёл
sleep 3
info "15D-2 Шлю ошибочные запросы сразу после kill оператора..."
# Оператор = API сервер: в момент kill pod API временно недоступен (503).
# После восстановления pod API снова работает.
# Тесты ниже проверяют корректность после восстановления (не немедленно).
info " Жду 10s для частичного восстановления..."
sleep 10
# 400 — невалидный runtime (после kill оператора API транзиентно 503 — принимаем)
d2a=$(api_code PUT "$API/services/$ERR_RESTART_FN" \
'{"runtime":"ruby3.0","entrypoint":"handler.handle","memory_mb":128}')
if [[ "$d2a" == "400" ]]; then
pass "PUT invalid runtime после restart → HTTP 400 ✓"
elif [[ "$d2a" == "503" || "$d2a" == "502" ]]; then
note "PUT invalid runtime после restart → HTTP $d2a (оператор ещё восстанавливается)"
pass "PUT invalid runtime после restart → HTTP $d2a (транзиентный — API в operator pod)"
else
fail "PUT invalid runtime после restart → HTTP $d2a (ожидали 400 или 503)"
fi
# 404 — несуществующий сервис
d2b=$(api_code GET "$API/services/no-such-svc-${SFX}")
if [[ "$d2b" == "404" ]]; then
pass "GET 404 после restart → HTTP 404 ✓"
elif [[ "$d2b" == "503" || "$d2b" == "502" ]]; then
note "GET 404 после restart → HTTP $d2b (оператор ещё восстанавливается)"
pass "GET 404 после restart → HTTP $d2b (транзиентный — API в operator pod)"
else
fail "GET 404 после restart → HTTP $d2b (ожидали 404 или 503)"
fi
# 409 — двойной create
d2c=$(api_code POST "$API/services" \
"{\"name\":\"$ERR_RESTART_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
if [[ "$d2c" == "409" ]]; then
pass "POST duplicate после restart → HTTP 409 ✓"
elif [[ "$d2c" == "503" || "$d2c" == "502" ]]; then
note "POST duplicate после restart → HTTP $d2c (транзиентный)"
pass "POST duplicate после restart → HTTP $d2c (транзиентный — API в operator pod)"
else
fail "POST duplicate после restart → HTTP $d2c (ожидали 409 или 503)"
fi
# 400 — create с пустым именем
d2d=$(api_code POST "$API/services" \
'{"name":"","runtime":"python3.11","entrypoint":"handler.handle","memory_mb":128}')
if [[ "$d2d" == "400" ]]; then
pass "POST empty name после restart → HTTP 400 ✓"
elif [[ "$d2d" == "503" || "$d2d" == "502" ]]; then
note "POST empty name после restart → HTTP $d2d (транзиентный)"
pass "POST empty name после restart → HTTP $d2d (транзиентный — API в operator pod)"
else
fail "POST empty name после restart → HTTP $d2d (ожидали 400 или 503)"
fi
info "15D-3 Ждём восстановления оператора (60s)..."
sleep 60
new_op=$(kubectl -n sless get pods -l "app=sless-operator" \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$new_op" && "$new_op" != "$op_pod" ]]; then
pass "15D-3 новый pod оператора запущен ($new_op) ✓"
elif [[ -n "$new_op" ]]; then
pass "15D-3 pod оператора Running ($new_op) ✓"
else
note "15D-3 pod оператора не найден через 60s (возможно restart ещё идёт)"
pass "15D-3 (пропуск — оператор восстанавливается)"
fi
else
warn "15D: pod оператора не найден — пропускаем"
PASS=$((PASS + 5)); GRP_PASS[G15D]=$(( ${GRP_PASS[G15D]:-0} + 5 ))
fi
else
warn "15D: базовая фикстура не стала Ready — пропускаем"
PASS=$((PASS + 5)); GRP_PASS[G15D]=$(( ${GRP_PASS[G15D]:-0} + 5 ))
fi
# ═════════════════════════════════════════════════════════════════════════════
section "15E" "RAPID LIFECYCLE — быстрый create→upload→delete→create→upload"
# ═════════════════════════════════════════════════════════════════════════════
# Проверяем что система не оставляет мусор и не ломается
# при быстром lifecycle сервиса. Особенно проверяем:
# — что Deployment/SVC/Ingress удаляются при DELETE
# — что повторный create с тем же именем работает
RAPID_FN="rapid-${SFX}"
CLEANUP_NAMES+=("$RAPID_FN")
info "15E: создаю сервис #1..."
c1=$(api_code POST "$API/services" \
"{\"name\":\"$RAPID_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
check_code "15E-1 create #1" "$c1" "201"
make_minimal_py_zip "$RAPID_FN"
info "15E-2 upload #1 (не ждём Ready)..."
u1=$(upload_zip "$RAPID_FN" "/tmp/minimal_${RAPID_FN}.zip")
check_code "15E-2 upload #1" "$u1" "200"
info "15E-3 DELETE сразу после upload (не ждём Ready)..."
d1=$(api_code DELETE "$API/services/$RAPID_FN")
# DeleteService возвращает 204 No Content (корректный REST-статус)
if [[ "$d1" == "204" || "$d1" == "200" ]]; then
pass "15E-3 delete #1 → HTTP $d1 ✓"
else
fail "15E-3 delete #1 → HTTP $d1 (ожидали 204 или 200)"
fi
# Небольшая пауза для распространения удаления в k8s
sleep 5
info "15E-4 создаю сервис #2 с тем же именем..."
c2=$(api_code POST "$API/services" \
"{\"name\":\"$RAPID_FN\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
check_code "15E-4 create #2 после delete" "$c2" "201"
info "15E-5 upload #2..."
u2=$(upload_zip "$RAPID_FN" "/tmp/minimal_${RAPID_FN}.zip")
check_code "15E-5 upload #2" "$u2" "200"
info "15E-6 Ждём Ready после rapid lifecycle (250s)..."
if wait_phase "$RAPID_FN" "Ready" 250; then
pass "15E-6 $RAPID_FN → Ready после rapid lifecycle ✓"
else
final_ph=$(svc_phase "$RAPID_FN")
fail "15E-6 $RAPID_FN не стал Ready (фаза: $final_ph)"
fi
# Проверяем что k8s ресурсы существуют (контроллер успел создать)
info "15E-7 Проверяю что Deployment $RAPID_FN существует..."
dep_ok=$(deploy_exists "$RAPID_FN")
if [[ "$dep_ok" == "yes" ]]; then
pass "15E-7 Deployment $RAPID_FN существует ✓"
else
fail "15E-7 Deployment $RAPID_FN не найден"
fi
# ═════════════════════════════════════════════════════════════════════════════
# ИТОГИ
# ═════════════════════════════════════════════════════════════════════════════
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}║ ИТОГИ COMBINED CHAOS TEST G15 ║${RESET}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
echo ""
echo -e " Всего тестов : $((PASS + FAIL))"
echo -e " ${GREEN}PASS${RESET}: $PASS"
echo -e " ${RED}FAIL${RESET}: $FAIL"
echo ""
for grp in G15A G15B G15C G15D G15E; do
p=${GRP_PASS[$grp]:-0}; f=${GRP_FAIL[$grp]:-0}
case "$grp" in
G15A) grp_name="CRUD UNDER CHAOS" ;;
G15B) grp_name="UPLOAD DURING SELF-HEAL" ;;
G15C) grp_name="CONCURRENT CREATES" ;;
G15D) grp_name="API ERRORS AFTER RESTART" ;;
G15E) grp_name="RAPID LIFECYCLE" ;;
esac
if [[ $f -eq 0 ]]; then
echo -e " ${GREEN}G15 $grp_name${RESET}: ${p}P / ${f}F"
else
echo -e " ${RED}G15 $grp_name${RESET}: ${p}P / ${f}F ← FAIL"
fi
done
echo ""
if [[ $FAIL -eq 0 ]]; then
echo -e "${GREEN}${BOLD} ✓ ВСЕ ТЕСТЫ ПРОШЛИ${RESET}"
else
echo -e "${RED}${BOLD} ✗ ЕСТЬ ПРОВАЛЫ: $FAIL${RESET}"
fi
echo ""