test: operator_failure_test.sh — group 12 failure modes (43/45)
This commit is contained in:
@@ -0,0 +1,862 @@
|
||||
#!/usr/bin/env bash
|
||||
# 2026-03-22 — operator_failure_test.sh
|
||||
# ТЕСТ ОТКАЗОУСТОЙЧИВОСТИ — Группа 12: FAILURE MODES
|
||||
#
|
||||
# Проверяет поведение оператора при ошибочном коде/конфиге, который мог
|
||||
# быть загружен пользователем по незнанию или небрежности:
|
||||
#
|
||||
# 12-A API VALIDATION — неверные параметры (memory, name, runtime) → 400
|
||||
# 12-B UPLOAD FAILURES — испорченный zip, неверный runtime → 400 на upload
|
||||
# 12-C BUILD FAILURES — несуществующий pip-пакет → канико падает → Failed
|
||||
# 12-D CRASH AT STARTUP — синтаксическая ошибка, неверный entrypoint,
|
||||
# отсутствующий модуль → контейнер падает при старте
|
||||
# (ДОКУМЕНТИРУЕТ ПРОБЕЛ: оператор не детектирует
|
||||
# CrashLoopBackOff, фаза остаётся "Ready")
|
||||
# 12-E RUNTIME EXCEPTIONS — handle() бросает исключение → 500
|
||||
# 12-F INVOKE TIMEOUT — handle() с бесконечным циклом → вызов зависает
|
||||
# 12-G EDGE CASES — memory_mb=1 принимается API; DELETE из Failed
|
||||
#
|
||||
# ВАЖНО ПО РЕЗУЛЬТАТАМ:
|
||||
# PASS — система ведёт себя как ожидается (в т.ч. корректно ловит ошибку)
|
||||
# FAIL — система ведёт себя некорректно (баг или пробел)
|
||||
# [NOTE] — поведение задокументировано, идеал отличается от факта
|
||||
#
|
||||
# ЗАПУСКАТЬ НА VM:
|
||||
# nohup bash ~/terra/sless/operator_failure_test.sh > /tmp/failure.log 2>&1 &
|
||||
# tail -f /tmp/failure.log
|
||||
#
|
||||
# Время прогона: ~25-40 минут (build failure ждёт kaniko ~3 мин).
|
||||
# Зависимости: curl, python3, zip, kubectl
|
||||
|
||||
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="fm-${TS}" # fm = failure modes
|
||||
|
||||
# ─── СТАТИСТИКА ──────────────────────────────────────────────────────────────
|
||||
|
||||
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'
|
||||
|
||||
# ─── HELPERS ─────────────────────────────────────────────────────────────────
|
||||
|
||||
_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 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
|
||||
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
|
||||
}
|
||||
|
||||
# wait_failed NAME TIMEOUT_SEC → 0=got Failed 1=timeout 2=got Ready unexpectedly
|
||||
wait_failed() {
|
||||
local name="$1" timeout_sec="${2:-180}"
|
||||
local deadline=$((SECONDS + timeout_sec))
|
||||
while [[ $SECONDS -lt $deadline ]]; do
|
||||
local phase; phase=$(svc_phase "$name")
|
||||
[[ "$phase" == "Failed" ]] && return 0
|
||||
[[ "$phase" == "Ready" ]] && return 2
|
||||
sleep 5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# upload_raw NAME ZIPFILE → HTTP code
|
||||
# Загружает zip через multipart form (стандартный upload endpoint)
|
||||
upload_raw() {
|
||||
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"
|
||||
}
|
||||
|
||||
# invoke_one NAME [TIMEOUT] → HTTP code (000 при таймауте/connection refused)
|
||||
invoke_one() {
|
||||
local name="$1" timeout="${2:-30}"
|
||||
curl -s -o /dev/null -w "%{http_code}" -m "$timeout" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{"msg":"failure-test"}' \
|
||||
"$FN_BASE/$name"
|
||||
}
|
||||
|
||||
# invoke_json NAME [TIMEOUT] → response body
|
||||
invoke_json() {
|
||||
local name="$1" timeout="${2:-30}"
|
||||
curl -s -m "$timeout" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{"msg":"failure-test"}' \
|
||||
"$FN_BASE/$name"
|
||||
}
|
||||
|
||||
# ─── ZIP GENERATORS ───────────────────────────────────────────────────────────
|
||||
|
||||
# make_corrupt_zip → /tmp/corrupt_${SFX}.zip (просто мусорные байты, не zip)
|
||||
make_corrupt_zip() {
|
||||
printf '\xff\xfe\x00\x01THISISNOTAVALIDZIPARCHIVE\xff\xfe\x00\x02garbage\n' \
|
||||
> "/tmp/corrupt_${SFX}.zip"
|
||||
}
|
||||
|
||||
# make_empty_valid_zip → /tmp/empty_${SFX}.zip (валидный zip но без файлов)
|
||||
make_empty_valid_zip() {
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
# zip пустой директории → архив с заголовком но без файлов
|
||||
(cd "$tmpdir" && touch PLACEHOLDER && zip -q "/tmp/empty_${SFX}.zip" PLACEHOLDER)
|
||||
# Удалим placeholder из архива — оставим структуру пустого zip
|
||||
rm -rf "$tmpdir"
|
||||
# Просто создадим минимальный zip без handler.py — это ключевой кейс
|
||||
local tmpdir2; tmpdir2=$(mktemp -d)
|
||||
cat > "$tmpdir2/README.txt" <<'EOF'
|
||||
No handler here. User forgot to put the actual code.
|
||||
EOF
|
||||
(cd "$tmpdir2" && zip -q "/tmp/empty_${SFX}.zip" README.txt)
|
||||
rm -rf "$tmpdir2"
|
||||
}
|
||||
|
||||
# make_bad_requirements_zip NAME → /tmp/badreq_${NAME}.zip
|
||||
# handler.py нормальный, requirements.txt с несуществующим пакетом →
|
||||
# pip install упадёт → kaniko job failed → phase=Failed
|
||||
make_bad_requirements_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
def handle(event):
|
||||
return {"ok": True, "msg": "i will never run"}
|
||||
PYEOF
|
||||
# Пакет надуманный — pip не найдёт его → build failure
|
||||
printf 'sless-nonexistent-pkg-xyz==999.0.0\n' > "$tmpdir/requirements.txt"
|
||||
(cd "$tmpdir" && zip -q "/tmp/badreq_${name}.zip" handler.py requirements.txt)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# make_syntax_error_zip NAME → /tmp/syntax_${NAME}.zip
|
||||
# handler.py с синтаксической ошибкой Python → exec_module() bails at startup
|
||||
make_syntax_error_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
# Пользователь перепутал скобки — классическая ошибка
|
||||
def handle(event)
|
||||
return {"ok": True}
|
||||
PYEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/syntax_${name}.zip" handler.py)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# make_bad_import_zip NAME → /tmp/badimport_${NAME}.zip
|
||||
# handler.py с top-level import несуществующего модуля →
|
||||
# exec_module() падает с ImportError → контейнер падает при старте
|
||||
make_bad_import_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
# Пользователь написал название библиотеки неправильно (опечатка)
|
||||
import sless_nonexistent_module_xyz_typo as client
|
||||
|
||||
def handle(event):
|
||||
return {"ok": True}
|
||||
PYEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/badimport_${name}.zip" handler.py)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# make_exception_zip NAME → /tmp/exception_${NAME}.zip
|
||||
# handle() всегда бросает исключение → server.py ловит → возвращает 500
|
||||
make_exception_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
def handle(event):
|
||||
# Пользователь забыл обработать ошибку и raise всегда срабатывает
|
||||
raise ValueError("something went wrong in user code: division by zero: " + str(1 // 0))
|
||||
PYEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/exception_${name}.zip" handler.py)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# make_infinite_zip NAME → /tmp/infinite_${NAME}.zip
|
||||
# handle() никогда не возвращается → invoke зависает навсегда
|
||||
make_infinite_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
import time
|
||||
|
||||
def handle(event):
|
||||
# Пользователь написал бесконечный цикл вместо конечного
|
||||
while True:
|
||||
time.sleep(1)
|
||||
PYEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/infinite_${name}.zip" handler.py)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# make_node_bad_export_zip NAME → /tmp/nodebad_${NAME}.zip
|
||||
# Node handler экспортирует неправильное имя — не exports.handle
|
||||
make_node_bad_export_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.js" <<'JSEOF'
|
||||
'use strict';
|
||||
// Пользователь перепутал имя экспорта — написал "run" вместо "handle"
|
||||
exports.run = function(event) {
|
||||
return { ok: true };
|
||||
};
|
||||
JSEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/nodebad_${name}.zip" handler.js)
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# make_minimal_py_zip NAME → /tmp/minimal_${NAME}.zip
|
||||
# Простой рабочий handler для базовых Deploy-проверок
|
||||
make_minimal_py_zip() {
|
||||
local name="$1"
|
||||
local tmpdir; tmpdir=$(mktemp -d)
|
||||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||||
def handle(event):
|
||||
return {"ok": True, "test": "failure_modes"}
|
||||
PYEOF
|
||||
(cd "$tmpdir" && zip -q "/tmp/minimal_${name}.zip" handler.py)
|
||||
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"
|
||||
}
|
||||
|
||||
# cleanup NAME — удалить сервис
|
||||
cleanup() {
|
||||
local name="$1"
|
||||
[[ -z "$name" ]] && return 0
|
||||
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
|
||||
sleep 2
|
||||
}
|
||||
|
||||
CLEANUP_NAMES=()
|
||||
teardown() {
|
||||
if [[ ${#CLEANUP_NAMES[@]} -gt 0 ]]; then
|
||||
echo -e "\n${CYAN}[TEARDOWN]${RESET} удаляю тестовые сервисы..."
|
||||
for name in "${CLEANUP_NAMES[@]}"; do
|
||||
[[ -z "$name" ]] && continue
|
||||
cleanup "$name"
|
||||
echo -e " ${CYAN}[TEARDOWN]${RESET} удалён: $name"
|
||||
done
|
||||
fi
|
||||
}
|
||||
trap teardown EXIT
|
||||
|
||||
# ─── СТАРТОВЫЙ БАННЕР ────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||||
echo -e "${BOLD}║ OPERATOR FAILURE MODES TEST — sless v0.1.49 ║${RESET}"
|
||||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||||
echo -e " Дата : $(TZ=Asia/Dubai date '+%Y-%m-%d %H:%M:%S') (GMT+4)"
|
||||
echo -e " API : $API"
|
||||
echo -e " SFX : $SFX"
|
||||
echo ""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "12A" "API VALIDATION — неверные параметры на входе"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: API должен возвращать 400 на явно мусорные запросы, не создавая CRD.
|
||||
|
||||
info "12A-1 memory_mb=0 → ожидаем 400..."
|
||||
check_code "memory_mb=0" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"val-test","runtime":"python3.11","entrypoint":"h.h","memory_mb":0}')" \
|
||||
"400"
|
||||
|
||||
info "12A-2 memory_mb=5000 (>4096) → ожидаем 400..."
|
||||
check_code "memory_mb=5000" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"val-test","runtime":"python3.11","entrypoint":"h.h","memory_mb":5000}')" \
|
||||
"400"
|
||||
|
||||
info "12A-3 memory_mb=-1 → ожидаем 400..."
|
||||
check_code "memory_mb=-1" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"val-test","runtime":"python3.11","entrypoint":"h.h","memory_mb":-1}')" \
|
||||
"400"
|
||||
|
||||
info "12A-4 name пустой → ожидаем 400..."
|
||||
check_code "name empty" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"","runtime":"python3.11","entrypoint":"h.h","memory_mb":128}')" \
|
||||
"400"
|
||||
|
||||
info "12A-5 runtime пустой → ожидаем 400..."
|
||||
check_code "runtime empty" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"val-test","runtime":"","entrypoint":"h.h","memory_mb":128}')" \
|
||||
"400"
|
||||
|
||||
info "12A-6 entrypoint пустой → ожидаем 400..."
|
||||
check_code "entrypoint empty" \
|
||||
"$(api_code POST "$API/services" \
|
||||
'{"name":"val-test","runtime":"python3.11","entrypoint":"","memory_mb":128}')" \
|
||||
"400"
|
||||
|
||||
info "12A-7 невалидный JSON body → ожидаем 400..."
|
||||
check_code "invalid JSON" \
|
||||
"$(api_code POST "$API/services" 'NOT JSON AT ALL {{{}')" \
|
||||
"400"
|
||||
|
||||
info "12A-8 memory_mb=1 → ожидаем 201 (API принимает, нет нижней границы)"
|
||||
code=$(api_code POST "$API/services" \
|
||||
'{"name":"val-mem1-'"${SFX}"'","runtime":"python3.11","entrypoint":"handler.handle","memory_mb":1}')
|
||||
if [[ "$code" == "201" ]]; then
|
||||
pass "memory_mb=1 → HTTP 201 (создан)"
|
||||
note "GAP: memory_mb=1 принимается API без предупреждения. Контейнер с 1Mi OOMKill'нется."
|
||||
# Удалим сразу — нам build не нужен
|
||||
api_code DELETE "$API/services/val-mem1-${SFX}" > /dev/null 2>&1 || true
|
||||
else
|
||||
fail "memory_mb=1 → HTTP $code (ожидали 201 — нет нижней границы в API)"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "12B" "UPLOAD FAILURES — испорченный zip / неверный runtime"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# 12B-1: Corrupt zip — случайные байты вместо zip-архива
|
||||
CORRUPT_FN="corrupt-${SFX}"
|
||||
CLEANUP_NAMES+=("$CORRUPT_FN")
|
||||
info "12B-1 Создаём функцию для corrupt zip test..."
|
||||
create_code=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$CORRUPT_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$create_code" == "201" ]]; then
|
||||
make_corrupt_zip
|
||||
info " Upload испорченного файла → ожидаем 400..."
|
||||
uc=$(upload_zip "$CORRUPT_FN" "/tmp/corrupt_${SFX}.zip")
|
||||
if [[ "$uc" == "400" ]]; then
|
||||
pass "corrupt zip → upload HTTP 400 (отклонён на этапе parse zip)"
|
||||
else
|
||||
fail "corrupt zip → upload HTTP $uc (ожидали 400)"
|
||||
fi
|
||||
info " DELETE функции после отклонённого upload → ожидаем 204..."
|
||||
check_code "DELETE after corrupt upload" \
|
||||
"$(api_code DELETE "$API/services/$CORRUPT_FN")" "204"
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$CORRUPT_FN}" )
|
||||
else
|
||||
fail "create $CORRUPT_FN → HTTP $create_code"
|
||||
fi
|
||||
|
||||
# 12B-2: Неверный runtime ("ruby3.0") — создаётся, но upload падает
|
||||
BADRT_FN="badrt-${SFX}"
|
||||
CLEANUP_NAMES+=("$BADRT_FN")
|
||||
info "12B-2 runtime='ruby3.0' → create ожидаем 201, upload ожидаем 400..."
|
||||
create_code2=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$BADRT_FN\",\"runtime\":\"ruby3.0\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$create_code2" == "201" ]]; then
|
||||
pass "runtime=ruby3.0 → create HTTP 201 (API не валидирует runtime имена)"
|
||||
note "GAP: API принимает любое runtime имя при создании; ошибка только на upload."
|
||||
# Загружаем минимальный zip — PrepareContext упадёт на runtimeBaseImage("ruby3.0")
|
||||
make_minimal_py_zip "$BADRT_FN"
|
||||
uc2=$(upload_zip "$BADRT_FN" "/tmp/minimal_${BADRT_FN}.zip")
|
||||
if [[ "$uc2" == "400" ]]; then
|
||||
pass "runtime=ruby3.0 → upload HTTP 400 (unsupported runtime отклонён в builder)"
|
||||
else
|
||||
fail "runtime=ruby3.0 → upload HTTP $uc2 (ожидали 400)"
|
||||
fi
|
||||
check_code "DELETE after bad runtime" \
|
||||
"$(api_code DELETE "$API/services/$BADRT_FN")" "204"
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$BADRT_FN}" )
|
||||
else
|
||||
fail "runtime=ruby3.0 → create HTTP $create_code2 (ожидали 201)"
|
||||
fi
|
||||
|
||||
# 12B-3: Пустой zip (без handler.py) — upload и build проходят, но контейнер падает
|
||||
EMPTY_FN="emptyzip-${SFX}"
|
||||
CLEANUP_NAMES+=("$EMPTY_FN")
|
||||
info "12B-3 Zip без handler.py → upload OK, build OK, но контейнер падает при старте..."
|
||||
create_code3=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$EMPTY_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$create_code3" == "201" ]]; then
|
||||
make_empty_valid_zip
|
||||
uc3=$(upload_zip "$EMPTY_FN" "/tmp/empty_${SFX}.zip")
|
||||
if [[ "$uc3" == "200" ]]; then
|
||||
pass "empty zip (без handler.py) → upload HTTP 200 (builder не проверяет наличие handler)"
|
||||
note "GAP: builder не требует наличия handler.py в zip. Build пройдёт, контейнер упадёт."
|
||||
info " Ждём Ready (build проходит успешно, 180s)..."
|
||||
if wait_phase "$EMPTY_FN" "Ready" 180; then
|
||||
pass "$EMPTY_FN → phase=Ready (build succeed без handler.py)"
|
||||
info " invoke → ожидаем сбой (FileNotFoundError при загрузке handler.py)..."
|
||||
sleep 5 # дать pod время запуститься и упасть
|
||||
inv_code=$(invoke_one "$EMPTY_FN" 15)
|
||||
if [[ "$inv_code" != "200" ]]; then
|
||||
pass "invoke без handler.py → HTTP $inv_code (не 200 — контейнер падает при старте)"
|
||||
note "GAP: фаза='Ready' но функция не работает. Оператор не детектирует CrashLoopBackOff."
|
||||
else
|
||||
fail "invoke без handler.py → HTTP 200 (неожиданно работает?)"
|
||||
fi
|
||||
else
|
||||
warn "$EMPTY_FN не достиг Ready — ждём ещё или уже Failed"
|
||||
fi
|
||||
else
|
||||
fail "empty zip → upload HTTP $uc3 (ожидали 200)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "12C" "BUILD FAILURES — несуществующий pip-пакет → phase=Failed"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: проверить что kaniko build failure → оператор переводит в Failed,
|
||||
# а не зависает в Building навсегда.
|
||||
|
||||
BADPIP_FN="badpip-${SFX}"
|
||||
CLEANUP_NAMES+=("$BADPIP_FN")
|
||||
info "12C-1 Создаём функцию с несуществующим pip-пакетом (sless-nonexistent-pkg-xyz)..."
|
||||
create_code4=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$BADPIP_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$create_code4" == "201" ]]; then
|
||||
make_bad_requirements_zip "$BADPIP_FN"
|
||||
uc4=$(upload_zip "$BADPIP_FN" "/tmp/badreq_${BADPIP_FN}.zip")
|
||||
if [[ "$uc4" == "200" ]]; then
|
||||
pass "bad requirements.txt → upload HTTP 200 (zip валидный, ошибка позже при kaniko)"
|
||||
info " Ждём phase=Failed (kaniko pip install упадёт, до 240s)..."
|
||||
wait_failed_result=0
|
||||
wait_failed "$BADPIP_FN" 240 || wait_failed_result=$?
|
||||
case "$wait_failed_result" in
|
||||
0) pass "bad requirements.txt → phase=Failed ✓ (kaniko build failure детектирован)" ;;
|
||||
2) fail "bad requirements.txt → phase=Ready (build как-то прошёл — неожиданно)" ;;
|
||||
1) fail "bad requirements.txt → timeout 240s, phase не стало Failed" ;;
|
||||
esac
|
||||
info " 12C-2 DELETE функции в Failed-состоянии → ожидаем 204..."
|
||||
check_code "DELETE in Failed state" \
|
||||
"$(api_code DELETE "$API/services/$BADPIP_FN")" "204"
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$BADPIP_FN}" )
|
||||
pass "DELETE из Failed → 204 (recovery возможен)"
|
||||
else
|
||||
fail "bad requirements.txt → upload HTTP $uc4 (ожидали 200)"
|
||||
fi
|
||||
else
|
||||
fail "create $BADPIP_FN → HTTP $create_code4"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "12D" "CRASH AT STARTUP — код ломает контейнер при загрузке"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Сценарии где build успешен, но контейнер падает при первом импорте module.
|
||||
# ФАКТ: оператор показывает phase=Ready, но invoke возвращает не-200.
|
||||
# ИДЕАЛ: оператор должен детектировать CrashLoopBackOff → Failed.
|
||||
|
||||
# 12D-1: Python syntax error
|
||||
SYNTAX_FN="syntax-${SFX}"
|
||||
CLEANUP_NAMES+=("$SYNTAX_FN")
|
||||
info "12D-1 Python syntax error в handler.py..."
|
||||
c=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$SYNTAX_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$c" == "201" ]]; then
|
||||
make_syntax_error_zip "$SYNTAX_FN"
|
||||
uc=$(upload_zip "$SYNTAX_FN" "/tmp/syntax_${SYNTAX_FN}.zip")
|
||||
if [[ "$uc" == "200" ]]; then
|
||||
pass "syntax error zip → upload 200 (парсер Python не вызывается при build)"
|
||||
info " Ждём Ready (180s)..."
|
||||
if wait_phase "$SYNTAX_FN" "Ready" 180; then
|
||||
pass "$SYNTAX_FN → phase=Ready (build прошёл — kaniko только копирует файлы)"
|
||||
note "GAP: сборка не проверяет синтаксис Python. Ошибка видна только при запуске контейнера."
|
||||
sleep 5
|
||||
inv_code=$(invoke_one "$SYNTAX_FN" 15)
|
||||
if [[ "$inv_code" != "200" ]]; then
|
||||
pass "syntax error → invoke HTTP $inv_code (не 200, контейнер упал на SyntaxError) ✓"
|
||||
note "GAP: фаза=Ready, но invoke ломается. Нет детектора CrashLoopBackOff в операторе."
|
||||
else
|
||||
fail "syntax error → invoke HTTP 200 (Python как-то выполнил невалидный код?)"
|
||||
fi
|
||||
else
|
||||
warn "$SYNTAX_FN не стал Ready — возможно Failed (неожиданно)"
|
||||
fi
|
||||
else
|
||||
fail "syntax error zip → upload HTTP $uc"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 12D-2: Неверное имя функции в entrypoint (handler.no_such_function)
|
||||
BADEP_FN="badep-${SFX}"
|
||||
CLEANUP_NAMES+=("$BADEP_FN")
|
||||
info "12D-2 Entrypoint 'handler.no_such_function' — функции не существует..."
|
||||
c2=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$BADEP_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.no_such_function\",\"memory_mb\":128}")
|
||||
if [[ "$c2" == "201" ]]; then
|
||||
make_minimal_py_zip "$BADEP_FN" # handler.py есть, но функция называется handle, не no_such_function
|
||||
uc2=$(upload_zip "$BADEP_FN" "/tmp/minimal_${BADEP_FN}.zip")
|
||||
if [[ "$uc2" == "200" ]]; then
|
||||
pass "wrong entrypoint → upload 200"
|
||||
info " Ждём Ready (180s)..."
|
||||
if wait_phase "$BADEP_FN" "Ready" 180; then
|
||||
pass "$BADEP_FN → phase=Ready"
|
||||
note "GAP: API не проверяет что entrypoint совпадает с функцией внутри zip."
|
||||
sleep 5
|
||||
inv_code2=$(invoke_one "$BADEP_FN" 15)
|
||||
if [[ "$inv_code2" != "200" ]]; then
|
||||
pass "wrong entrypoint → invoke HTTP $inv_code2 (не 200, AttributeError при старте) ✓"
|
||||
else
|
||||
fail "wrong entrypoint → invoke HTTP 200 (как вызвал несуществующую функцию?)"
|
||||
fi
|
||||
else
|
||||
warn "$BADEP_FN не Ready"
|
||||
fi
|
||||
else
|
||||
fail "wrong entrypoint zip → upload HTTP $uc2"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 12D-3: Top-level неверный import (опечатка в имени библиотеки)
|
||||
BADIMPORT_FN="badimport-${SFX}"
|
||||
CLEANUP_NAMES+=("$BADIMPORT_FN")
|
||||
info "12D-3 'import sless_nonexistent_module_xyz_typo' на верхнем уровне handler.py..."
|
||||
c3=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$BADIMPORT_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$c3" == "201" ]]; then
|
||||
make_bad_import_zip "$BADIMPORT_FN"
|
||||
uc3=$(upload_zip "$BADIMPORT_FN" "/tmp/badimport_${BADIMPORT_FN}.zip")
|
||||
if [[ "$uc3" == "200" ]]; then
|
||||
pass "bad import zip → upload 200"
|
||||
info " Ждём Ready (180s)..."
|
||||
if wait_phase "$BADIMPORT_FN" "Ready" 180; then
|
||||
pass "$BADIMPORT_FN → phase=Ready (build не проверяет импорты)"
|
||||
note "GAP: отсутствующий import на верхнем уровне не виден до старта контейнера."
|
||||
sleep 5
|
||||
inv_code3=$(invoke_one "$BADIMPORT_FN" 15)
|
||||
if [[ "$inv_code3" != "200" ]]; then
|
||||
pass "bad import → invoke HTTP $inv_code3 (не 200, ImportError при старте) ✓"
|
||||
else
|
||||
fail "bad import → invoke HTTP 200 (неожиданно — модуль как-то нашёлся?)"
|
||||
fi
|
||||
else
|
||||
warn "$BADIMPORT_FN не Ready"
|
||||
fi
|
||||
else
|
||||
fail "bad import zip → upload HTTP $uc3"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 12D-4: Node.js — неправильный exports.name (exports.run вместо exports.handle)
|
||||
NODEBAD_FN="nodebad-${SFX}"
|
||||
CLEANUP_NAMES+=("$NODEBAD_FN")
|
||||
info "12D-4 Node.js: exports.run вместо exports.handle (неверное имя экспорта)..."
|
||||
c4=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$NODEBAD_FN\",\"runtime\":\"nodejs20\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$c4" == "201" ]]; then
|
||||
make_node_bad_export_zip "$NODEBAD_FN"
|
||||
uc4=$(upload_zip "$NODEBAD_FN" "/tmp/nodebad_${NODEBAD_FN}.zip")
|
||||
if [[ "$uc4" == "200" ]]; then
|
||||
pass "Node.js wrong export → upload 200"
|
||||
info " Ждём Ready (180s)..."
|
||||
if wait_phase "$NODEBAD_FN" "Ready" 180; then
|
||||
pass "$NODEBAD_FN → phase=Ready"
|
||||
note "GAP: Node.js server.js делает process.exit(1) при неверном экспорте → CrashLoop → Ready всё равно."
|
||||
sleep 5
|
||||
inv_code4=$(invoke_one "$NODEBAD_FN" 15)
|
||||
if [[ "$inv_code4" != "200" ]]; then
|
||||
pass "Node.js wrong export → invoke HTTP $inv_code4 (не 200) ✓"
|
||||
else
|
||||
fail "Node.js wrong export → invoke HTTP 200 (как?"
|
||||
fi
|
||||
else
|
||||
warn "$NODEBAD_FN не Ready"
|
||||
fi
|
||||
else
|
||||
fail "Node.js wrong export → upload HTTP $uc4"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "12E" "RUNTIME EXCEPTIONS — handle() бросает исключения"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Здесь ошибка происходит внутри handle() — server.py ловит через try/except.
|
||||
# Ожидаемое поведение: invoke возвращает 500, а не EOF/502.
|
||||
|
||||
EXC_FN="exception-${SFX}"
|
||||
CLEANUP_NAMES+=("$EXC_FN")
|
||||
info "12E-1 Создаём функцию с handle() который всегда бросает ValueError..."
|
||||
c5=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$EXC_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$c5" == "201" ]]; then
|
||||
make_exception_zip "$EXC_FN"
|
||||
uc5=$(upload_zip "$EXC_FN" "/tmp/exception_${EXC_FN}.zip")
|
||||
if [[ "$uc5" == "200" ]]; then
|
||||
pass "exception zip → upload 200"
|
||||
info " Ждём Ready (180s)..."
|
||||
if wait_phase "$EXC_FN" "Ready" 180; then
|
||||
pass "$EXC_FN → phase=Ready"
|
||||
sleep 3
|
||||
info " 12E-2 invoke → ожидаем 500 (exception в handle() пойман server.py)..."
|
||||
inv_code5=$(invoke_one "$EXC_FN" 30)
|
||||
if [[ "$inv_code5" == "500" ]]; then
|
||||
pass "exception → invoke HTTP 500 ✓ (server.py поймал ValueError, вернул JSON error)"
|
||||
else
|
||||
fail "exception → invoke HTTP $inv_code5 (ожидали 500)"
|
||||
fi
|
||||
info " 12E-3 Повторный invoke → тоже 500, функция не зависает..."
|
||||
inv_code5b=$(invoke_one "$EXC_FN" 30)
|
||||
check_code "repeat exception invoke" "$inv_code5b" "500"
|
||||
info " 12E-4 Ответ 500 содержит поле error с описанием исключения..."
|
||||
response=$(invoke_json "$EXC_FN" 30)
|
||||
has_error=$(echo "$response" | python3 -c \
|
||||
'import sys,json; d=json.load(sys.stdin); print("error" in d)' 2>/dev/null)
|
||||
if [[ "$has_error" == "True" ]]; then
|
||||
pass "500 response содержит поле 'error' ✓"
|
||||
else
|
||||
fail "500 response не содержит поле 'error': $response"
|
||||
fi
|
||||
else
|
||||
warn "$EXC_FN не Ready"
|
||||
fi
|
||||
else
|
||||
fail "exception zip → upload HTTP $uc5"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "12F" "INVOKE TIMEOUT — handle() с бесконечным циклом"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Цель: invoke зависает навсегда, вызывающий получает таймаут (не 502/EOF).
|
||||
# Это тест НА ПОВЕДЕНИЕ вызывающего — оператор не может ограничить выполнение.
|
||||
|
||||
INF_FN="infinite-${SFX}"
|
||||
CLEANUP_NAMES+=("$INF_FN")
|
||||
info "12F-1 Создаём функцию с бесконечным циклом (while True: sleep(1))..."
|
||||
c6=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$INF_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$c6" == "201" ]]; then
|
||||
make_infinite_zip "$INF_FN"
|
||||
uc6=$(upload_zip "$INF_FN" "/tmp/infinite_${INF_FN}.zip")
|
||||
if [[ "$uc6" == "200" ]]; then
|
||||
pass "infinite loop zip → upload 200"
|
||||
info " Ждём Ready (180s)..."
|
||||
if wait_phase "$INF_FN" "Ready" 180; then
|
||||
pass "$INF_FN → phase=Ready"
|
||||
sleep 3
|
||||
info " 12F-2 invoke с таймаутом 8s → ожидаем код 000 (curl timeout)..."
|
||||
start_ts=$SECONDS
|
||||
inf_code=$(invoke_one "$INF_FN" 8)
|
||||
elapsed=$((SECONDS - start_ts))
|
||||
if [[ "$inf_code" == "000" ]]; then
|
||||
pass "infinite loop → invoke timeout за ${elapsed}s, код 000 ✓"
|
||||
note "INFO: оператор не ограничивает время выполнения функции. Ответственность на вызывающем."
|
||||
elif [[ "$inf_code" != "200" ]]; then
|
||||
pass "infinite loop → invoke HTTP $inf_code (не 200, прервало ingress/proxy) за ${elapsed}s"
|
||||
else
|
||||
fail "infinite loop → invoke HTTP 200 за ${elapsed}s (неожиданно вернулся)"
|
||||
fi
|
||||
info " 12F-3 Функция доступна после таймаутного запроса (новый invoke ещё может зависнуть)..."
|
||||
# Проверим что pod живой (не упал) и GET /health работает
|
||||
health_code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
|
||||
"$FN_BASE/$INF_FN" 2>/dev/null || echo "000")
|
||||
if [[ "$health_code" != "200" ]]; then
|
||||
pass "функция принимает новые запросы после предыдущего таймаута (висит снова: $health_code)"
|
||||
fi
|
||||
else
|
||||
warn "$INF_FN не Ready"
|
||||
fi
|
||||
else
|
||||
fail "infinite loop zip → upload HTTP $uc6"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "12G" "EDGE CASES — компоновка крайних случаев"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# 12G-1: DELETE функции которая осталась в Pending (после failed upload)
|
||||
PEND_DEL_FN="pendel-${SFX}"
|
||||
CLEANUP_NAMES+=("$PEND_DEL_FN")
|
||||
info "12G-1 Создаём функцию, не загружаем код, сразу DELETE → ожидаем 204..."
|
||||
api_code POST "$API/services" \
|
||||
"{\"name\":\"$PEND_DEL_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
|
||||
sleep 1
|
||||
check_code "DELETE Pending function (no upload)" \
|
||||
"$(api_code DELETE "$API/services/$PEND_DEL_FN")" "204"
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$PEND_DEL_FN}" )
|
||||
|
||||
# 12G-2: Повторное создание с тем же именем после удаления — нет конфликта
|
||||
REUSE_FN="reuse-${SFX}"
|
||||
CLEANUP_NAMES+=("$REUSE_FN")
|
||||
info "12G-2 Создаём, удаляем, повторно создаём с тем же именем → нет конфликта..."
|
||||
c_r1=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$REUSE_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
sleep 1
|
||||
api_code DELETE "$API/services/$REUSE_FN" > /dev/null 2>&1
|
||||
sleep 5 # даём finalizer-у отработать
|
||||
c_r2=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$REUSE_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$c_r1" == "201" && "$c_r2" == "201" ]]; then
|
||||
pass "повторное создание '$REUSE_FN' → 201 (нет конфликта имени после DELETE) ✓"
|
||||
else
|
||||
fail "повторное создание: первое=$c_r1 второе=$c_r2 (ожидали оба 201)"
|
||||
fi
|
||||
api_code DELETE "$API/services/$REUSE_FN" > /dev/null 2>&1
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$REUSE_FN}" )
|
||||
|
||||
# 12G-3: PUT (update) с memory_mb=0 на существующую функцию → 400
|
||||
UPD_FN="update-${SFX}"
|
||||
CLEANUP_NAMES+=("$UPD_FN")
|
||||
info "12G-3 PUT update с memory_mb=0 на существующую функцию → ожидаем 400..."
|
||||
api_code POST "$API/services" \
|
||||
"{\"name\":\"$UPD_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
|
||||
sleep 1
|
||||
check_code "PUT memory_mb=0 on existing function" \
|
||||
"$(api_code PUT "$API/services/$UPD_FN" \
|
||||
'{"runtime":"python3.11","entrypoint":"handler.handle","memory_mb":0}')" \
|
||||
"400"
|
||||
api_code DELETE "$API/services/$UPD_FN" > /dev/null 2>&1
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$UPD_FN}" )
|
||||
|
||||
# 12G-4: GET несуществующего сервиса → 404
|
||||
info "12G-4 GET несуществующего сервиса → ожидаем 404..."
|
||||
check_code "GET nonexistent function" \
|
||||
"$(api_code GET "$API/services/totally-nonexistent-fn-xyz-${SFX}")" "404"
|
||||
|
||||
# 12G-5: DELETE несуществующего сервиса → 404
|
||||
info "12G-5 DELETE несуществующего сервиса → ожидаем 404..."
|
||||
check_code "DELETE nonexistent function" \
|
||||
"$(api_code DELETE "$API/services/totally-nonexistent-fn-xyz-${SFX}")" "404"
|
||||
|
||||
# 12G-6: Дублированное создание (POST с тем же именем без delete) → 409 или 422
|
||||
DUP_FN="dup-${SFX}"
|
||||
CLEANUP_NAMES+=("$DUP_FN")
|
||||
info "12G-6 Создать дубль (POST с тем же именем без delete) → ожидаем 409/422..."
|
||||
api_code POST "$API/services" \
|
||||
"{\"name\":\"$DUP_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" > /dev/null
|
||||
sleep 1
|
||||
dup_code=$(api_code POST "$API/services" \
|
||||
"{\"name\":\"$DUP_FN\",\"runtime\":\"python3.11\",\
|
||||
\"entrypoint\":\"handler.handle\",\"memory_mb\":128}")
|
||||
if [[ "$dup_code" == "409" || "$dup_code" == "422" || "$dup_code" == "400" ]]; then
|
||||
pass "duplicate create → HTTP $dup_code (дубль отклонён) ✓"
|
||||
else
|
||||
fail "duplicate create → HTTP $dup_code (ожидали 409/422, допустимо 400)"
|
||||
fi
|
||||
api_code DELETE "$API/services/$DUP_FN" > /dev/null 2>&1
|
||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$DUP_FN}" )
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||||
echo -e "${BOLD}║ ИТОГИ FAILURE MODES TEST ║${RESET}"
|
||||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||||
echo ""
|
||||
echo " Время завершения : $(TZ=Asia/Dubai date '+%Y-%m-%d %H:%M:%S') (GMT+4)"
|
||||
echo ""
|
||||
echo " Всего тестов : $((PASS + FAIL))"
|
||||
echo " PASS : $PASS"
|
||||
echo " FAIL : $FAIL"
|
||||
echo ""
|
||||
|
||||
for grp in G12A G12B G12C G12D G12E G12F G12G; do
|
||||
g_pass=${GRP_PASS[$grp]:-0}
|
||||
g_fail=${GRP_FAIL[$grp]:-0}
|
||||
g_total=$((g_pass + g_fail))
|
||||
label=""
|
||||
case "$grp" in
|
||||
G12A) label="API VALIDATION";; G12B) label="UPLOAD FAILURES";;
|
||||
G12C) label="BUILD FAILURES";; G12D) label="CRASH AT STARTUP";;
|
||||
G12E) label="RUNTIME EXCEPTIONS";; G12F) label="INVOKE TIMEOUT";;
|
||||
G12G) label="EDGE CASES";;
|
||||
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}✓ FAILURE MODES — система корректно обрабатывает все сценарии${RESET}"
|
||||
else
|
||||
echo -e " ${RED}${BOLD}✗ FAILURE MODES — $FAIL тестов упало (изучить [FAIL] строки выше)${RESET}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${YELLOW}[NOTE]${RESET} Строки [NOTE] выше описывают документированные пробелы в операторе,"
|
||||
echo -e " не обязательно баги — это известное поведение для улучшения."
|
||||
echo ""
|
||||
Reference in New Issue
Block a user