#!/bin/bash # 2026-03-21 — full_test.sh: комплексный тест всех sless-ресурсов. # # Фазы: # 1. CRUD — проверяем наличие всех сервисов через API # 2. Функциональные — корректность ответов, правильные значения # 3. PG-стресс — параллельные write/read, pgstorm (Go), js-async storm # 4. Краш-шторм — параллельные паники, проверяем что платформа жива после # # Запуск: bash full_test.sh # Зависимости: curl, python3, terraform (для CRUD destroy/create) # # Среда: namespace sless-ffd1f598c169b0ae, токен в ~/terra/sless/test.token set -uo pipefail TOKEN=$(cat /home/naeel/terra/sless/test.token) NS="sless-ffd1f598c169b0ae" BASE="https://sless.kube5s.ru/fn/$NS" API="https://sless.kube5s.ru/v1/namespaces/$NS" GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' PASS=0 FAIL=0 pass() { echo -e " ${GREEN}[PASS]${NC} $1"; ((PASS++)); } fail() { echo -e " ${RED}[FAIL]${NC} $1"; ((FAIL++)); } section() { echo -e "\n${YELLOW}━━━ $1 ━━━${NC}"; } info() { echo -e " ${CYAN}[INFO]${NC} $1"; } # Вызвать URL и вернуть JSON (не проверяя код) call() { local url="$1" body="${2:-}" extra_headers="${3:-}" local args=(-s -m 90 -H "Authorization: Bearer $TOKEN") [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") [[ -n "$extra_headers" ]] && args+=(-H "$extra_headers") curl "${args[@]}" "$url" } # Проверить HTTP-код (только код, без тела) check_http() { local label="$1" url="$2" method="${3:-GET}" body="${4:-}" expect="${5:-200}" local args=(-s -o /dev/null -w "%{http_code}" -m 90 -H "Authorization: Bearer $TOKEN") [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") [[ "$method" != "GET" ]] && args+=(-X "$method") local code code=$(curl "${args[@]}" "$url") if [[ "$code" == "$expect" ]]; then pass "$label → HTTP $code" else fail "$label → HTTP $code (ожидали $expect)" fi } # Вызвать функцию, проверить поле JSON == expected check_field() { local label="$1" url="$2" body="$3" field="$4" expected="$5" local resp resp=$(call "$url" "$body") local actual actual=$(echo "$resp" | python3 -c " import sys, json try: d = json.load(sys.stdin) v = d.get('$field', '__MISSING__') print(str(v)) except Exception as e: print('PARSE_ERROR: ' + str(e)) " 2>/dev/null) if [[ "$actual" == "$expected" ]]; then pass "$label" else fail "$label → got '$actual' (ожидали '$expected') | resp: $(echo "$resp" | head -c 200)" fi } # Вызвать функцию, проверить что поле JSON > 0 (числовое) check_field_gt0() { local label="$1" url="$2" body="$3" field="$4" local resp resp=$(call "$url" "$body") local actual actual=$(echo "$resp" | python3 -c " import sys, json try: d = json.load(sys.stdin) v = d.get('$field', 0) print(1 if float(str(v)) > 0 else 0) except: print(0) " 2>/dev/null) if [[ "$actual" == "1" ]]; then pass "$label" else fail "$label → resp: $(echo "$resp" | head -c 200)" fi } # ═══════════════════════════════════════════════════════════════ section "ФАЗА 1: CRUD — проверяем что все сервисы существуют" # ═══════════════════════════════════════════════════════════════ ALL_SERVICES=( pg-info pg-table-reader pg-table-writer stress-go-fast stress-go-nil stress-go-pgstorm stress-js-async stress-js-badenv stress-slow stress-bigloop stress-divzero stress-writer pg-stats ) for svc in "${ALL_SERVICES[@]}"; do code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \ -H "Authorization: Bearer $TOKEN" "$API/services/$svc") if [[ "$code" == "200" ]]; then pass "API GET /services/$svc → 200" else fail "API GET /services/$svc → $code" fi done info "Проверяем несуществующий сервис → 404" check_http "GET /services/THIS-SERVICE-DOES-NOT-EXIST → 404" \ "$API/services/this-service-does-not-exist" "GET" "" "404" info "Проверяем jobs" code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \ -H "Authorization: Bearer $TOKEN" "$API/jobs/pg-create-table-job-main-v13") if [[ "$code" == "200" ]]; then pass "API GET /jobs/pg-create-table-job-main-v13 → 200" else fail "API GET /jobs/pg-create-table-job-main-v13 → $code" fi # ═══════════════════════════════════════════════════════════════ section "ФАЗА 2: Функциональные тесты (корректность ответов)" # ═══════════════════════════════════════════════════════════════ info "── Go 1.23 ──" # stress-go-fast: factorial(10) = 3628800 check_field "go-fast runtime=go1.23" \ "$BASE/stress-go-fast" '{"n":10}' "runtime" "go1.23" check_field "go-fast factorial(10)=3628800" \ "$BASE/stress-go-fast" '{"n":10}' "factorial" "3628800" check_field "go-fast fib(10)=55" \ "$BASE/stress-go-fast" '{"n":10}' "fib" "55" # n>20 обрезается до 20 — проверяем граничный случай check_field "go-fast n=21 обрезается до 20: fib(20)=6765" \ "$BASE/stress-go-fast" '{"n":21}' "fib" "6765" # stress-go-nil crash=false → crashed:false check_field "go-nil crash=false → crashed=False" \ "$BASE/stress-go-nil" '{"crash":false}' "crashed" "False" # stress-go-nil crash=true → 500 check_http "go-nil crash=true → HTTP 500" \ "$BASE/stress-go-nil" "POST" '{"crash":true}' "500" # stress-go-nil default (no body) → 500 (по умолчанию crash=true) check_http "go-nil без параметров → HTTP 500" \ "$BASE/stress-go-nil" "GET" "" "500" info "── Node.js 20 ──" # stress-js-async: чтение PG, возвращает pg_version check_field "js-async runtime=nodejs20" \ "$BASE/stress-js-async" "" "runtime" "nodejs20" check_field_gt0 "js-async total_rows > 0" \ "$BASE/stress-js-async" "" "total_rows" # stress-js-badenv crash=false → ok check_field "js-badenv crash=false → runtime=nodejs20" \ "$BASE/stress-js-badenv" '{"crash":false}' "runtime" "nodejs20" # stress-js-badenv crash=true → 500 check_http "js-badenv crash=true → HTTP 500" \ "$BASE/stress-js-badenv" "POST" '{"crash":true}' "500" info "── Python 3.11 ──" # stress-slow check_field "slow: slept_sec=3" \ "$BASE/stress-slow" '{"sleep":3}' "slept_sec" "3" check_field "slow: version=v1" \ "$BASE/stress-slow" '{"sleep":1}' "version" "v1" # stress-bigloop: sum(i*i for i in range(10)) = 285 check_field "bigloop n=10 sum_of_squares=285" \ "$BASE/stress-bigloop" '{"n":10}' "sum_of_squares" "285" # range(100): 0+1+4+...+9801 = sum(i^2,0..99) = 99*100*199/6 = 328350 check_field "bigloop n=100 sum_of_squares=328350" \ "$BASE/stress-bigloop" '{"n":100}' "sum_of_squares" "328350" # stress-divzero 42/7 = 6.0 check_field "divzero 42/7=6.0" \ "$BASE/stress-divzero" '{"n":42,"d":7}' "result" "6.0" # divzero d=0 → 500 check_http "divzero d=0 → HTTP 500" \ "$BASE/stress-divzero" "POST" '{"n":1,"d":0}' "500" # stress-writer: записывает 3 строки check_field "writer rows=3 → count=3" \ "$BASE/stress-writer" '{"rows":3,"prefix":"functional-test"}' "count" "3" check_field "writer rows=1 → count=1" \ "$BASE/stress-writer" '{"rows":1,"prefix":"functional-single"}' "count" "1" # pg-stats check_field "pg-stats version=v1-test7" \ "$BASE/pg-stats" "" "version" "v1-test7" check_field_gt0 "pg-stats total_rows > 0" \ "$BASE/pg-stats" "" "total_rows" # pg-info (nodejs) check_field "pg-info runtime=nodejs20" \ "$BASE/pg-info" "" "runtime" "nodejs20" # pg-table-reader check_http "table-reader HTTP 200" "$BASE/pg-table-reader" READER_RESP=$(call "$BASE/pg-table-reader") READER_COUNT=$(echo "$READER_RESP" | python3 -c " import sys, json try: d = json.load(sys.stdin) print(d.get('count', 0)) except: print(0) " 2>/dev/null) if [[ "$READER_COUNT" -gt 0 ]] 2>/dev/null; then pass "table-reader count=$READER_COUNT строк" else fail "table-reader ожидали >0 строк, получили: $READER_COUNT | $(echo "$READER_RESP" | head -c 200)" fi # pg-table-writer: POST JSON должен вставить строку и вернуть JSON info "pg-table-writer POST (ожидаем JSON если платформа инжектит _method)" WRITER_RESP=$(curl -s -m 30 -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"title":"full-test-insert-2026"}' \ "$BASE/pg-table-writer") WRITER_OK=$(echo "$WRITER_RESP" | python3 -c " import sys, json try: d = json.load(sys.stdin) print(d.get('ok', False)) except: print('NOT_JSON') " 2>/dev/null) if [[ "$WRITER_OK" == "True" ]]; then pass "table-writer POST → ok=True, строка вставлена" else # HTML ответ — платформа не инжектит _method info "table-writer вернул не JSON (вероятно HTML), ok=$WRITER_OK" info "resp: $(echo "$WRITER_RESP" | head -c 100)" # Это не баг, но фиксируем как наблюдение fi # ═══════════════════════════════════════════════════════════════ section "ФАЗА 3: PG-стресс (параллельная нагрузка на PostgreSQL)" # ═══════════════════════════════════════════════════════════════ info "Запуск 40 параллельных stress-writer × 5 строк = 200 INSERT..." ROWS_BEFORE=$(echo "$READER_COUNT") WRITER_PIDS=() for i in $(seq 1 40); do curl -s -m 60 -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"rows\":5,\"prefix\":\"pgstorm-w$i\"}" \ "$BASE/stress-writer" > "/tmp/sw_$i.json" 2>&1 & WRITER_PIDS+=($!) done wait "${WRITER_PIDS[@]}" WRITER_OK=0; WRITER_FAIL=0 for i in $(seq 1 40); do cnt=$(python3 -c " import json try: d = json.load(open('/tmp/sw_$i.json')) print(d.get('count', 0)) except: print(0) " 2>/dev/null) if [[ "$cnt" == "5" ]]; then ((WRITER_OK++)) else ((WRITER_FAIL++)) info " writer batch $i: cnt=$cnt | $(cat /tmp/sw_$i.json | head -c 150)" fi done info "writer: $WRITER_OK/40 OK, $WRITER_FAIL failed" [[ "$WRITER_FAIL" == "0" ]] \ && pass "40× parallel writer: все 40 вернули count=5 (200 строк)" \ || fail "40× parallel writer: $WRITER_FAIL пакетов с ошибкой" # Проверим что строки реально появились в таблице NEW_COUNT=$(call "$BASE/pg-stats" | python3 -c " import sys, json try: print(json.load(sys.stdin).get('total_rows', 0)) except: print(0) ") info "pg-stats: total_rows=$NEW_COUNT (было $ROWS_BEFORE до stress)" [[ "$NEW_COUNT" -gt "$ROWS_BEFORE" ]] \ && pass "pg-stats: строки выросли ($ROWS_BEFORE → $NEW_COUNT)" \ || fail "pg-stats: строки не выросли ($ROWS_BEFORE → $NEW_COUNT)" info "Запуск 30 параллельных stress-js-async (3 PG-запроса каждый = 90 одновременных)..." JS_PIDS=() for i in $(seq 1 30); do curl -s -m 30 -H "Authorization: Bearer $TOKEN" \ "$BASE/stress-js-async" > "/tmp/jsa_$i.json" 2>&1 & JS_PIDS+=($!) done wait "${JS_PIDS[@]}" JS_OK=0; JS_FAIL=0 for i in $(seq 1 30); do rt=$(python3 -c " import json try: print(json.load(open('/tmp/jsa_$i.json')).get('runtime', 'err')) except: print('err') " 2>/dev/null) if [[ "$rt" == "nodejs20" ]]; then ((JS_OK++)); else ((JS_FAIL++)); fi done [[ "$JS_FAIL" == "0" ]] \ && pass "30× parallel js-async: все 30 OK" \ || fail "30× parallel js-async: $JS_OK ok, $JS_FAIL failed" info "Запуск stress-go-pgstorm workers=50 duration=45s..." PGSTORM_RESP=$(curl -s -m 120 \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"workers":50,"duration_sec":45,"max_delay_ms":50}' \ "$BASE/stress-go-pgstorm") PGSTORM_OK=$(echo "$PGSTORM_RESP" | python3 -c " import sys, json try: d = json.load(sys.stdin) print(d.get('ok_ops', 0)) except: print(0) ") PGSTORM_ERR=$(echo "$PGSTORM_RESP" | python3 -c " import sys, json try: d = json.load(sys.stdin) print(d.get('err_ops', 0)) except: print(-1) ") PGSTORM_RPS=$(echo "$PGSTORM_RESP" | python3 -c " import sys, json try: d = json.load(sys.stdin) print(d.get('ops_per_sec', '?')) except: print('?') ") info "pgstorm: ok=$PGSTORM_OK err=$PGSTORM_ERR ops/s=$PGSTORM_RPS" [[ "$PGSTORM_OK" -gt 0 ]] 2>/dev/null \ && pass "stress-go-pgstorm: $PGSTORM_OK ops OK, $PGSTORM_ERR err, $PGSTORM_RPS ops/s" \ || fail "stress-go-pgstorm: 0 операций | $(echo "$PGSTORM_RESP" | head -c 300)" # Итоговая статистика таблицы FINAL_STATS=$(call "$BASE/pg-stats") FINAL_ROWS=$(echo "$FINAL_STATS" | python3 -c " import sys, json try: print(json.load(sys.stdin).get('total_rows', 0)) except: print(0) ") info "Итого строк в terraform_demo_table: $FINAL_ROWS" # ═══════════════════════════════════════════════════════════════ section "ФАЗА 4: Краш-шторм (параллельные паники — платформа должна жить)" # ═══════════════════════════════════════════════════════════════ info "25× го-nil crash + 25× divzero + 25× js-badenv = 75 параллельных крашей..." CRASH_PIDS=() for i in $(seq 1 25); do curl -s -o /dev/null -w "%{http_code}" -m 15 \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"crash":true}' "$BASE/stress-go-nil" > "/tmp/c_nil_$i.txt" 2>&1 & CRASH_PIDS+=($!) curl -s -o /dev/null -w "%{http_code}" -m 15 \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"n":1,"d":0}' "$BASE/stress-divzero" > "/tmp/c_dz_$i.txt" 2>&1 & CRASH_PIDS+=($!) curl -s -o /dev/null -w "%{http_code}" -m 15 \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"crash":true}' "$BASE/stress-js-badenv" > "/tmp/c_js_$i.txt" 2>&1 & CRASH_PIDS+=($!) done wait "${CRASH_PIDS[@]}" C500=0; CNOT500=0 for i in $(seq 1 25); do for f in "/tmp/c_nil_$i.txt" "/tmp/c_dz_$i.txt" "/tmp/c_js_$i.txt"; do code=$(cat "$f" 2>/dev/null || echo "0") if [[ "$code" == "500" ]]; then ((C500++)); else ((CNOT500++)); info " неожиданный $f: code=$code"; fi done done info "Краши: $C500 × 500, $CNOT500 неожиданных" [[ "$CNOT500" == "0" ]] \ && pass "75× краш-шторм: все вернули HTTP 500 (платформа устойчива)" \ || fail "75× краш-шторм: $CNOT500 ответов не 500" info "Проверяем что сервисы живы после краш-шторма..." check_http "go-fast: жив после штормов" "$BASE/stress-go-fast" "GET" "" "200" check_http "js-async: жив после штормов" "$BASE/stress-js-async" "GET" "" "200" check_http "pg-table-reader: жив после штормов" "$BASE/pg-table-reader" "GET" "" "200" check_http "pg-stats: жив после штормов" "$BASE/pg-stats" "GET" "" "200" # ═══════════════════════════════════════════════════════════════ section "ИТОГИ" # ═══════════════════════════════════════════════════════════════ echo "" TOTAL=$((PASS + FAIL)) echo -e " Всего тестов: $TOTAL" echo -e " ${GREEN}PASS: $PASS${NC}" echo -e " ${RED}FAIL: $FAIL${NC}" echo "" if [[ "$FAIL" == "0" ]]; then echo -e " ${GREEN}✓ ВСЕ ТЕСТЫ ПРОШЛИ${NC}" exit 0 else echo -e " ${RED}✗ ЕСТЬ ПАДЕНИЯ ($FAIL)${NC}" exit 1 fi