Files
sless/run_stress_test.sh
T

663 lines
30 KiB
Bash
Executable File

#!/usr/bin/env bash
# 2026-03-11
# run_stress_test.sh — Полный стресс-тест всех examples.
#
# Что прогоняем на каждом примере:
# [ROUND 1] init → apply → HTTP-проверка → destroy → проверка "упал"
# [ROUND 2] apply → modify (memory) → apply(modify) → HTTP-проверка
# → modify (env_var) → apply(modify2) → HTTP-проверка
# → modify (timeout) → apply(modify3)
# → destroy → проверка "упал"
# [ROUND 3] apply → destroy (сразу, без ожидания Ready)
# [hello-node extra] job re-run: run_id bump → apply → assert Succeeded
# [simple-* extra] trigger disabled → apply → assert 404 → enabled → apply → assert 200
# [notes-python] CRUD: add note → list → assert запись есть → destroy
#
# Логи каждого шага: .stress-logs/<example>-<step>-<attempt>.log
# При ошибке скрипт не падает — пишет FAIL и продолжает следующий пример.
# В конце — сводная таблица PASS/FAIL по каждому примеру.
#
# Запуск:
# ./run_stress_test.sh
# ./run_stress_test.sh hello-node simple-node # конкретные примеры
#
# Время: ~30-50 мин (зависит от скорости сборки канико)
set -uo pipefail
# ── Конфигурация ──────────────────────────────────────────────────────────────
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXAMPLES_DIR="$REPO_ROOT/examples"
LOGS_DIR="$REPO_ROOT/.stress-logs"
mkdir -p "$LOGS_DIR"
API_BASE="https://sless-api.kube5s.ru"
NS="sless-cdd874dfa31ba6ca" # вычислен из токена; не меняется
# ── Примеры ───────────────────────────────────────────────────────────────────
if [ $# -gt 0 ]; then
EXAMPLES=("$@")
else
EXAMPLES=("hello-node" "simple-node" "simple-python" "notes-python")
fi
# ── Цвета / вывод ─────────────────────────────────────────────────────────────
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
ok() { echo -e "${GREEN}$*${RESET}"; }
fail() { echo -e "${RED}$*${RESET}"; }
info() { echo -e "${YELLOW}$*${RESET}"; }
step() { echo -e "${CYAN} [step] $*${RESET}"; }
header(){ echo -e "\n${BOLD}${CYAN}══════════════════════════════════════════${RESET}"; \
echo -e "${BOLD}${CYAN} $*${RESET}"; \
echo -e "${BOLD}${CYAN}══════════════════════════════════════════${RESET}"; }
# ── Результаты ────────────────────────────────────────────────────────────────
declare -A RESULTS=() # example → "PASS" | "FAIL: <reason>"
declare -A TIMINGS=() # example → секунды
# ── Emergency destroy trap ────────────────────────────────────────────────────
ACTIVE_EXAMPLE=""
cleanup_trap() {
if [ -n "$ACTIVE_EXAMPLE" ]; then
local dir="$EXAMPLES_DIR/$ACTIVE_EXAMPLE"
if [ -f "$dir/terraform.tfstate" ] && \
python3 -c "
import json,sys
d=json.load(open('$dir/terraform.tfstate'))
sys.exit(0 if d.get('resources') else 1)
" 2>/dev/null; then
echo -e "\n${RED}[trap] Script interrupted — emergency destroy: $ACTIVE_EXAMPLE${RESET}"
(cd "$dir" && terraform destroy -auto-approve -input=false -no-color \
>> "$LOGS_DIR/${ACTIVE_EXAMPLE}-emergency-destroy.log" 2>&1) || true
fi
restore_all_backups "$ACTIVE_EXAMPLE"
fi
}
trap cleanup_trap EXIT INT TERM
# ── Утилиты: terraform ────────────────────────────────────────────────────────
# tf_run <logfile> <chdir> <args...>
# Ретраит до 4 раз при сетевых ошибках (TLS timeout, EOF, i/o timeout)
tf_run() {
local logfile="$1"; local chdir="$2"; shift 2
local attempt=1 max=4
while [ "$attempt" -le "$max" ]; do
: > "$logfile"
if (cd "$chdir" && terraform "$@" -no-color) 2>&1 | tee "$logfile"; then
return 0
fi
local rc=${PIPESTATUS[0]}
if grep -Eiq \
'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline exceeded|Client\.Timeout' \
"$logfile" && [ "$attempt" -lt "$max" ]; then
info "network hiccup (attempt $attempt/$max), retry in $((attempt*3))s..."
sleep $((attempt * 3))
attempt=$((attempt + 1))
continue
fi
return "$rc"
done
}
tf_init() { tf_run "$1" "$2" init -input=false -upgrade; }
tf_apply() { tf_run "$1" "$2" apply -auto-approve -input=false; }
tf_destroy() { tf_run "$1" "$2" destroy -auto-approve -input=false; }
assert_apply_ok() { grep -q 'Apply complete' "$1"; }
assert_destroy_ok() { grep -q 'Destroy complete' "$1"; }
assert_no_changes() { grep -q 'No changes' "$1"; }
# ── Утилиты: HTTP-проверки ────────────────────────────────────────────────────
# http_check <url> [expected_http_code] [max_attempts] [sleep_sec]
http_check() {
local url="$1"
local expected="${2:-200}"
local attempts="${3:-12}"
local sleep_sec="${4:-10}"
local try=1
while [ "$try" -le "$attempts" ]; do
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$url" 2>/dev/null || echo "000")
if [ "$code" = "$expected" ]; then
return 0
fi
info "HTTP $code (want $expected), waiting... ($try/$attempts)"
try=$((try + 1))
sleep "$sleep_sec"
done
fail "HTTP check failed: got $code, wanted $expected after $((attempts*sleep_sec))s"
return 1
}
# http_post <url> <json_body> → возвращает тело в stdout
http_post() {
curl -sS -X POST -H 'Content-Type: application/json' -d "$2" \
--max-time 15 "$1" 2>/dev/null
}
http_get() {
curl -sS --max-time 15 "$1" 2>/dev/null
}
# ── Утилиты: бэкапы и патчи ───────────────────────────────────────────────────
backup_file() {
local f="$1"
cp "$f" "$f.stress.bak"
}
restore_all_backups() {
local example="$1"
local dir="$EXAMPLES_DIR/$example"
while IFS= read -r bak; do
[ -f "$bak" ] || continue
mv "$bak" "${bak%.stress.bak}"
done < <(find "$dir" -name '*.stress.bak' 2>/dev/null)
}
patch_memory() {
local file="$1" from="$2" to="$3"
perl -pi -e "s/(memory_mb\s+=\s+)$from/\${1}$to/" "$file"
}
patch_timeout() {
local file="$1" from="$2" to="$3"
perl -pi -e "s/(timeout_sec\s+=\s+)$from/\${1}$to/" "$file"
}
patch_enabled() {
local file="$1" val="$2" # true|false
perl -pi -e "s/(enabled\s+=\s+)(true|false)/\${1}$val/" "$file"
}
# Добавить/заменить блок env_vars в .tf файле
# Вставляет после строки содержащей "source_dir"
add_env_var() {
local file="$1" key="$2" val="$3"
if grep -q "^ env_vars" "$file"; then
# уже есть — добавляем строку внутрь блока
perl -pi -e "s/(env_vars\s*=\s*\{)/\${1}\n $key = \"$val\"/" "$file"
else
# нет — вставляем перед source_dir
perl -pi -e "s/( source_dir)/ env_vars = \{ $key = \"$val\" \}\n\${1}/" "$file"
fi
}
remove_env_vars_block() {
local file="$1"
perl -0pi -e 's/\s*env_vars\s*=\s*\{[^}]*\}//g' "$file"
}
# ── Утилиты: terraform output ─────────────────────────────────────────────────
tf_output() {
local chdir="$1" key="$2"
(cd "$chdir" && terraform output -raw "$key" 2>/dev/null) || echo ""
}
# ── Утилиты: счётчик шагов ────────────────────────────────────────────────────
STEP_NUM=0
STEP_FAILS=0
next_step() {
STEP_NUM=$((STEP_NUM + 1))
step "[$STEP_NUM] $*"
}
assert_step() {
local desc="$1"; shift
if "$@"; then
ok "$desc"
return 0
else
fail "$desc"
STEP_FAILS=$((STEP_FAILS + 1))
return 1
fi
}
# ── Чистка артефактов ─────────────────────────────────────────────────────────
clean_artifacts() {
local dir="$1"
rm -rf "$dir/.terraform" "$dir/.terraform.lock.hcl" \
"$dir/terraform.tfstate" "$dir/terraform.tfstate.backup" \
"$dir"/terraform.tfstate.*.backup
}
# ══════════════════════════════════════════════════════════════════════════════
# ТЕСТ: hello-node
# ══════════════════════════════════════════════════════════════════════════════
test_hello_node() {
local ex="hello-node"
local dir="$EXAMPLES_DIR/$ex"
local log="$LOGS_DIR/$ex"
ACTIVE_EXAMPLE="$ex"
STEP_NUM=0; STEP_FAILS=0
local t0=$SECONDS
header "hello-node (nodejs20 · HTTP trigger + Job)"
restore_all_backups "$ex"
clean_artifacts "$dir"
# ── init ──────────────────────────────────────────────────────────────────
next_step "terraform init"
assert_step "init OK" tf_init "${log}-init.log" "$dir" || { RESULTS[$ex]="FAIL: init"; return 1; }
# ── ROUND 1: чистый цикл ─────────────────────────────────────────────────
next_step "[R1] terraform apply"
assert_step "apply OK" tf_apply "${log}-r1-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R1 apply"; return 1; }
assert_step "Apply complete in log" assert_apply_ok "${log}-r1-apply.log"
local trigger_url
trigger_url=$(tf_output "$dir" "trigger_url")
next_step "[R1] HTTP check — trigger alive (url: $trigger_url)"
assert_step "HTTP 200" http_check "$trigger_url" 200 15 10
next_step "[R1] Job result check"
local job_phase
job_phase=$(tf_output "$dir" "job_phase")
assert_step "Job phase = Succeeded" [ "$job_phase" = "Succeeded" ]
local job_msg
job_msg=$(tf_output "$dir" "job_message")
assert_step "Job message contains sum" echo "$job_msg" | grep -q '"sum"'
info "Job output: $job_msg"
next_step "[R1] terraform destroy"
assert_step "destroy OK" tf_destroy "${log}-r1-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R1 destroy"; return 1; }
assert_step "Destroy complete in log" assert_destroy_ok "${log}-r1-destroy.log"
next_step "[R1] HTTP check — trigger gone (want 404/502+unreachable)"
# после destroy endpoint должен исчезнуть или вернуть 404
local gone=0
for i in $(seq 1 18); do
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$trigger_url" 2>/dev/null || echo "000")
if [ "$code" = "404" ] || [ "$code" = "000" ]; then
gone=1; break
fi
info " still up (HTTP $code), wait 5s... ($i/18)"
sleep 5
done
assert_step "endpoint gone after destroy" [ "$gone" -eq 1 ]
# ── ROUND 2: apply → 3 модификации → destroy ─────────────────────────────
next_step "[R2] terraform apply (fresh)"
assert_step "apply OK" tf_apply "${log}-r2-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R2 apply"; return 1; }
trigger_url=$(tf_output "$dir" "trigger_url")
# Mod 1: memory 128 → 256
next_step "[R2-mod1] memory_mb 128→256 в http.tf"
backup_file "$dir/http.tf"
patch_memory "$dir/http.tf" 128 256
assert_step "apply mod1 OK" tf_apply "${log}-r2-mod1.log" "$dir"
assert_step "mod1: 1 changed" grep -q '1 changed' "${log}-r2-mod1.log"
assert_step "HTTP 200 after mod1" http_check "$trigger_url" 200 10 8
# Mod 2: добавить env_var + убрать
next_step "[R2-mod2] добавить env_var GREETING"
backup_file "$dir/job.tf" # на случай если патч затронет и его
add_env_var "$dir/http.tf" "GREETING" "stress-test-v1"
assert_step "apply mod2 OK" tf_apply "${log}-r2-mod2.log" "$dir"
assert_step "HTTP 200 after mod2" http_check "$trigger_url" 200 10 8
info " mod2 changes: $(grep -E 'changed|updated' "${log}-r2-mod2.log" | head -2)"
# Mod 3: timeout 30 → 45
next_step "[R2-mod3] timeout_sec 30→45 в http.tf"
patch_timeout "$dir/http.tf" 30 45
assert_step "apply mod3 OK" tf_apply "${log}-r2-mod3.log" "$dir"
assert_step "HTTP 200 after mod3" http_check "$trigger_url" 200 10 8
# Mod 4: trigger.enabled false → функция остаётся, триггер отключается
next_step "[R2-mod4] trigger enabled=false"
patch_enabled "$dir/http.tf" false
assert_step "apply mod4 OK" tf_apply "${log}-r2-mod4.log" "$dir"
# enabled=false → Deployment replicas=0 → /fn/ вернёт 503 или пустой ответ
info " trigger disabled — проверяем что НЕ 200"
local code_after_disable
code_after_disable=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$trigger_url" 2>/dev/null || echo "000")
ok " HTTP $code_after_disable (trigger disabled)"
# Mod 5: trigger enabled=true обратно
next_step "[R2-mod5] trigger enabled=true (restore)"
patch_enabled "$dir/http.tf" true
assert_step "apply mod5 OK" tf_apply "${log}-r2-mod5.log" "$dir"
assert_step "HTTP 200 after re-enable" http_check "$trigger_url" 200 12 10
# Job re-run: bump run_id 9 → 10
next_step "[R2-job-rerun] job re-run (run_id 9→10)"
backup_file "$dir/job.tf"
perl -pi -e 's/(run_id\s+=\s+)9/${1}10/' "$dir/job.tf"
assert_step "apply job-rerun OK" tf_apply "${log}-r2-job-rerun.log" "$dir"
local new_job_phase
new_job_phase=$(tf_output "$dir" "job_phase")
assert_step "re-run Succeeded" [ "$new_job_phase" = "Succeeded" ]
local new_job_msg
new_job_msg=$(tf_output "$dir" "job_message")
info " re-run output: $new_job_msg"
# Восстанавливаем файлы до оригинала перед destroy
restore_all_backups "$ex"
next_step "[R2] terraform destroy"
assert_step "destroy OK" tf_destroy "${log}-r2-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R2 destroy"; return 1; }
assert_step "Destroy complete" assert_destroy_ok "${log}-r2-destroy.log"
# ── ROUND 3: apply → немедленный destroy (no-wait) ───────────────────────
next_step "[R3] apply → immediate destroy"
assert_step "R3 apply OK" tf_apply "${log}-r3-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R3 apply"; return 1; }
assert_step "R3 destroy OK" tf_destroy "${log}-r3-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R3 destroy"; return 1; }
clean_artifacts "$dir"
TIMINGS[$ex]=$((SECONDS - t0))
if [ "$STEP_FAILS" -eq 0 ]; then
RESULTS[$ex]="PASS"
ok "hello-node все шаги PASS (${TIMINGS[$ex]}s)"
else
RESULTS[$ex]="FAIL: $STEP_FAILS step(s) failed"
fail "hello-node: $STEP_FAILS шагов с ошибкой"
fi
ACTIVE_EXAMPLE=""
}
# ══════════════════════════════════════════════════════════════════════════════
# ТЕСТ: simple-node / simple-python (одинаковая логика, разный runtime)
# ══════════════════════════════════════════════════════════════════════════════
test_simple() {
local ex="$1" # simple-node | simple-python
local dir="$EXAMPLES_DIR/$ex"
local log="$LOGS_DIR/$ex"
ACTIVE_EXAMPLE="$ex"
STEP_NUM=0; STEP_FAILS=0
local t0=$SECONDS
header "$ex"
restore_all_backups "$ex"
clean_artifacts "$dir"
# init
next_step "terraform init"
assert_step "init OK" tf_init "${log}-init.log" "$dir" || { RESULTS[$ex]="FAIL: init"; return 1; }
# ── ROUND 1 ───────────────────────────────────────────────────────────────
next_step "[R1] apply"
assert_step "apply OK" tf_apply "${log}-r1-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R1 apply"; return 1; }
local display_url
display_url=$(tf_output "$dir" "display_url")
next_step "[R1] HTTP check — display alive"
assert_step "HTTP 200" http_check "$display_url" 200 15 10
local job_result
job_result=$(tf_output "$dir" "job_result")
assert_step "job_result contains time" echo "$job_result" | grep -q '"time"'
info " job result: $job_result"
next_step "[R1] destroy"
assert_step "destroy OK" tf_destroy "${log}-r1-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R1 destroy"; return 1; }
next_step "[R1] endpoint gone check"
local gone=0
for i in $(seq 1 18); do
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$display_url" 2>/dev/null || echo "000")
if [ "$code" = "404" ] || [ "$code" = "000" ]; then gone=1; break; fi
info " still $code, wait 5s... ($i/18)"
sleep 5
done
assert_step "endpoint gone" [ "$gone" -eq 1 ]
# ── ROUND 2: три модификации ──────────────────────────────────────────────
next_step "[R2] apply (fresh)"
assert_step "apply OK" tf_apply "${log}-r2-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R2 apply"; return 1; }
display_url=$(tf_output "$dir" "display_url")
# Mod 1: memory 64 → 96 в time-display.tf
next_step "[R2-mod1] memory 64→96 (time-display)"
backup_file "$dir/time-display.tf"
patch_memory "$dir/time-display.tf" 64 96
assert_step "apply mod1 OK" tf_apply "${log}-r2-mod1.log" "$dir"
assert_step "1 changed" grep -q '1 changed' "${log}-r2-mod1.log"
assert_step "HTTP 200 after mod1" http_check "$display_url" 200 12 10
# Mod 2: memory 64 → 96 в time-getter.tf
next_step "[R2-mod2] memory 64→96 (time-getter)"
backup_file "$dir/time-getter.tf"
patch_memory "$dir/time-getter.tf" 64 96
assert_step "apply mod2 OK" tf_apply "${log}-r2-mod2.log" "$dir"
assert_step "1 changed" grep -q '1 changed' "${log}-r2-mod2.log"
assert_step "HTTP 200 after mod2" http_check "$display_url" 200 10 8
# Mod 3: добавить env_var в time-display.tf
next_step "[R2-mod3] добавить env_var TEST_LABEL в time-display"
add_env_var "$dir/time-display.tf" "TEST_LABEL" "stress-round2"
assert_step "apply mod3 OK" tf_apply "${log}-r2-mod3.log" "$dir"
assert_step "HTTP 200 after mod3" http_check "$display_url" 200 10 8
# Mod 4: job re-run (run_id 1 → 2)
next_step "[R2-mod4] job re-run (run_id 1→2)"
backup_file "$dir/time-getter.tf"
perl -pi -e 's/(run_id\s+=\s+)1/${1}2/' "$dir/time-getter.tf"
assert_step "apply mod4 (job rerun) OK" tf_apply "${log}-r2-mod4.log" "$dir"
local new_job
new_job=$(tf_output "$dir" "job_result")
assert_step "re-run result has time" echo "$new_job" | grep -q '"time"'
info " re-run result: $new_job"
assert_step "HTTP 200 after job rerun" http_check "$display_url" 200 10 8
# Mod 5: timeout 30 → 60 в time-display.tf
next_step "[R2-mod5] timeout 30→60 (time-display)"
patch_timeout "$dir/time-display.tf" 30 60
assert_step "apply mod5 OK" tf_apply "${log}-r2-mod5.log" "$dir"
restore_all_backups "$ex"
next_step "[R2] destroy"
assert_step "destroy OK" tf_destroy "${log}-r2-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R2 destroy"; return 1; }
# ── ROUND 3: immediate destroy ────────────────────────────────────────────
next_step "[R3] apply → immediate destroy"
assert_step "R3 apply OK" tf_apply "${log}-r3-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R3 apply"; return 1; }
assert_step "R3 destroy OK" tf_destroy "${log}-r3-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R3 destroy"; return 1; }
clean_artifacts "$dir"
TIMINGS[$ex]=$((SECONDS - t0))
if [ "$STEP_FAILS" -eq 0 ]; then
RESULTS[$ex]="PASS"
ok "$ex все шаги PASS (${TIMINGS[$ex]}s)"
else
RESULTS[$ex]="FAIL: $STEP_FAILS step(s) failed"
fail "$ex: $STEP_FAILS шагов с ошибкой"
fi
ACTIVE_EXAMPLE=""
}
# ══════════════════════════════════════════════════════════════════════════════
# ТЕСТ: notes-python (postgres + CRUD)
# ══════════════════════════════════════════════════════════════════════════════
test_notes_python() {
local ex="notes-python"
local dir="$EXAMPLES_DIR/$ex"
local log="$LOGS_DIR/$ex"
ACTIVE_EXAMPLE="$ex"
STEP_NUM=0; STEP_FAILS=0
local t0=$SECONDS
header "notes-python (python3.11 · PostgreSQL CRUD)"
restore_all_backups "$ex"
clean_artifacts "$dir"
next_step "terraform init"
assert_step "init OK" tf_init "${log}-init.log" "$dir" || { RESULTS[$ex]="FAIL: init"; return 1; }
# ── ROUND 1 ───────────────────────────────────────────────────────────────
next_step "[R1] apply (DB init + CRUD deploy)"
assert_step "apply OK" tf_apply "${log}-r1-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R1 apply"; return 1; }
# Проверяем DB init джобы
local tbl_phase idx_phase
tbl_phase=$(tf_output "$dir" "db_init_table_status" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase','?'))" 2>/dev/null || echo "?")
idx_phase=$(tf_output "$dir" "db_init_index_status" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase','?'))" 2>/dev/null || echo "?")
next_step "[R1] DB init jobs check"
assert_step "table init Succeeded" [ "$tbl_phase" = "Succeeded" ]
assert_step "index init Succeeded" [ "$idx_phase" = "Succeeded" ]
local notes_url notes_list_url
notes_url=$(tf_output "$dir" "notes_url")
notes_list_url=$(tf_output "$dir" "notes_list_url")
next_step "[R1] HTTP check — notes alive"
assert_step "notes HTTP 200" http_check "$notes_url" 200 15 10
assert_step "notes-list HTTP 200" http_check "$notes_list_url" 200 10 8
# CRUD: добавить несколько записей
next_step "[R1] CRUD: add notes"
local add1 add2 add3
add1=$(http_post "${notes_url}/add?title=StressTest1&body=body1" '{}')
add2=$(http_post "${notes_url}/add?title=StressTest2&body=body2" '{}')
add3=$(http_post "${notes_url}/add?title=StressTest3&body=body3" '{}')
assert_step "add note1 OK" echo "$add1" | grep -qiE '"id"|"created|created_at'
assert_step "add note2 OK" echo "$add2" | grep -qiE '"id"|"created|created_at'
assert_step "add note3 OK" echo "$add3" | grep -qiE '"id"|"created|created_at'
info " add1: $add1"
info " add2: $add2"
# List: проверить что записи есть
next_step "[R1] CRUD: list notes"
local listed
listed=$(http_get "$notes_list_url")
assert_step "list contains StressTest" echo "$listed" | grep -q 'StressTest'
info " list (первые 200 символов): ${listed:0:200}"
# Update nota1 (если вернулся id)
local note_id
note_id=$(echo "$add1" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || echo "")
if [ -n "$note_id" ]; then
next_step "[R1] CRUD: update note id=$note_id"
local updated
updated=$(http_post "${notes_url}/update?id=${note_id}&title=StressUpdated&body=updated_body" '{}')
assert_step "update OK" echo "$updated" | grep -qiE '"updated|ok|success|StressUpdated'
info " update: $updated"
fi
# ── ROUND 2: модификации ──────────────────────────────────────────────────
# Mod 1: memory 128→192 в notes.tf
next_step "[R2-mod1] memory 128→192 (notes.tf)"
backup_file "$dir/notes.tf"
patch_memory "$dir/notes.tf" 128 192
assert_step "apply mod1 OK" tf_apply "${log}-r2-mod1.log" "$dir"
assert_step "1 changed" grep -q '1 changed' "${log}-r2-mod1.log"
assert_step "notes HTTP 200 after mod1" http_check "$notes_url" 200 12 10
# Mod 2: memory 128→192 в notes-list.tf
next_step "[R2-mod2] memory 128→192 (notes-list.tf)"
backup_file "$dir/notes-list.tf"
patch_memory "$dir/notes-list.tf" 128 192
assert_step "apply mod2 OK" tf_apply "${log}-r2-mod2.log" "$dir"
assert_step "notes-list HTTP 200 after mod2" http_check "$notes_list_url" 200 12 10
# Mod 3: добавить env_var в notes.tf
next_step "[R2-mod3] env_var MAX_NOTES=100 в notes.tf"
add_env_var "$dir/notes.tf" "MAX_NOTES" "100"
assert_step "apply mod3 OK" tf_apply "${log}-r2-mod3.log" "$dir"
assert_step "HTTP 200 after mod3" http_check "$notes_url" 200 12 10
# Mod 4: timeout 30→60 в notes.tf
next_step "[R2-mod4] timeout 30→60 (notes.tf)"
patch_timeout "$dir/notes.tf" 30 60
assert_step "apply mod4 OK" tf_apply "${log}-r2-mod4.log" "$dir"
# CRUD после модификаций
next_step "[R2] CRUD: list после модификаций"
listed=$(http_get "$notes_list_url")
assert_step "list still works" echo "$listed" | grep -qiE '"id"|\[\]'
info " list: ${listed:0:200}"
next_step "[R2] CRUD: add ещё запись"
local add4
add4=$(http_post "${notes_url}/add?title=PostMod&body=after_modifications" '{}')
assert_step "add after mod OK" echo "$add4" | grep -qiE '"id"|"created'
restore_all_backups "$ex"
next_step "[R2] destroy"
assert_step "destroy OK" tf_destroy "${log}-r2-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R2 destroy"; return 1; }
# ── ROUND 3: apply → immediate destroy ───────────────────────────────────
next_step "[R3] apply → immediate destroy"
assert_step "R3 apply OK" tf_apply "${log}-r3-apply.log" "$dir" || { RESULTS[$ex]="FAIL: R3 apply"; return 1; }
assert_step "R3 destroy OK" tf_destroy "${log}-r3-destroy.log" "$dir" || { RESULTS[$ex]="FAIL: R3 destroy"; return 1; }
clean_artifacts "$dir"
TIMINGS[$ex]=$((SECONDS - t0))
if [ "$STEP_FAILS" -eq 0 ]; then
RESULTS[$ex]="PASS"
ok "notes-python все шаги PASS (${TIMINGS[$ex]}s)"
else
RESULTS[$ex]="FAIL: $STEP_FAILS step(s) failed"
fail "notes-python: $STEP_FAILS шагов с ошибкой"
fi
ACTIVE_EXAMPLE=""
}
# ══════════════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════════════
main() {
local total_t0=$SECONDS
echo -e "\n${BOLD}${CYAN}"
echo "╔══════════════════════════════════════════════╗"
echo "║ sless STRESS TEST $(date '+%Y-%m-%d %H:%M:%S') ║"
echo "╚══════════════════════════════════════════════╝"
echo -e "${RESET}"
echo "Примеры : ${EXAMPLES[*]}"
echo "Логи : $LOGS_DIR"
echo
echo "На каждый пример:"
echo " R1: apply → HTTP-check → destroy → endpoint-gone-check"
echo " R2: apply → 4-5 модификаций (memory, env_var, timeout, re-run) → destroy"
echo " R3: apply → immediate destroy"
echo
for ex in "${EXAMPLES[@]}"; do
case "$ex" in
hello-node) test_hello_node || true ;;
simple-node) test_simple "$ex" || true ;;
simple-python) test_simple "$ex" || true ;;
notes-python) test_notes_python || true ;;
*) fail "Unknown example: $ex"; RESULTS[$ex]="SKIP: unknown" ;;
esac
done
# ── Финальная сводка ──────────────────────────────────────────────────────
local total_elapsed=$((SECONDS - total_t0))
echo
echo -e "${BOLD}${CYAN}════════════════ ИТОГ ════════════════${RESET}"
local all_pass=1
for ex in "${EXAMPLES[@]}"; do
local result="${RESULTS[$ex]:-UNKNOWN}"
local timing="${TIMINGS[$ex]:-?}"
if [ "$result" = "PASS" ]; then
ok "$(printf '%-18s' "$ex") ${result} (${timing}s)"
else
fail "$(printf '%-18s' "$ex") ${result} (${timing}s)"
all_pass=0
fi
done
echo -e "${BOLD}${CYAN}══════════════════════════════════════${RESET}"
echo "Общее время: ${total_elapsed}s"
echo "Логи: $LOGS_DIR"
echo
if [ "$all_pass" -eq 1 ]; then
echo -e "${GREEN}${BOLD}✓ Все тесты прошли успешно.${RESET}"
exit 0
else
echo -e "${RED}${BOLD}✗ Есть ошибки — см. логи выше и в $LOGS_DIR${RESET}"
exit 1
fi
}
main