#!/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 ""