test: G_MULTIUSER — 10 parallel users + Postgres (ready to run)
This commit is contained in:
+19
-19
@@ -13,30 +13,30 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TODO (завтра — приоритет)
|
## TODO (в работе — сессия 9)
|
||||||
|
|
||||||
### ПЛАН: multi-user тест с Postgres
|
### G_MULTIUSER: 10 параллельных пользователей с Postgres
|
||||||
|
|
||||||
**Шаг 1 — Добавить Postgres в survival тест (один юзер):**
|
**Статус:** пишем `operator_multiuser_test.sh`
|
||||||
- Функции в `operator_survival_test.sh` сейчас echo-хендлеры без PG
|
|
||||||
- Добавить группу тестов где функция делает реальные INSERT/SELECT в Postgres
|
|
||||||
- Postgres креды берутся из параметров ресурса `sless_pg` (через `env_vars` функции)
|
|
||||||
- Изоляция: таблица называется `test_<namespace>` — каждый юзер пишет в свою
|
|
||||||
- Прогнать от одного юзера → убедиться что работает
|
|
||||||
|
|
||||||
**Шаг 2 — Параметризовать survival тест:**
|
**Реализация (упрощённая, без SHA256):**
|
||||||
- `TOKEN` и `NAMESPACE` через env переменные (сейчас хардкодные)
|
- 10 фейковых JWT: sub=`test-user-01`..`test-user-10` (подпись не проверяется оператором)
|
||||||
- `PG_DSN` через env переменную (берётся из параметров Postgres ресурса)
|
- Namespace: `sless-mu01`..`sless-mu10` (создаются через `/ensure`)
|
||||||
|
- Каждый пользователь создаёт сервис `pg-muXX` с Python 3.11 + psycopg2
|
||||||
|
- Функция INSERT/SELECT в таблицу `test_sless_muXX` (изоляция по таблице)
|
||||||
|
- Параллельный запуск 10 пользователей через `&` + `wait`
|
||||||
|
|
||||||
**Шаг 3 — Обёртка на 10 юзеров:**
|
**Технические детали:**
|
||||||
- `operator_multiuser_test.sh`
|
- PG host: `postgresqlk8s-master.bba55e0f-4c51-4312-8ad3-efc557de331d.svc.cluster.local`
|
||||||
- Генерирует 10 фейковых JWT (`test-user-01` ... `test-user-10`)
|
- PG creds: `user0 / db0` (из `examples/POSTGRES/terraform.tfvars`)
|
||||||
- Для каждого вычисляет namespace (SHA256(sub)[:8 байт])
|
- `sless-fn-{ns}` создаётся автоматически контроллером при первом сервисе
|
||||||
- `POST /v1/namespaces/{ns}/ensure` — создаёт namespace
|
- Таймаут Kaniko: 900s (psycopg2-binary тяжёлый пакет)
|
||||||
- Параллельно запускает полный survival тест в каждом namespace (`&`)
|
- Ожидаемый результат: **80 тестов (8×10)**
|
||||||
- Ждёт всех (`wait`), собирает итоговый отчёт: PASS/FAIL по каждому юзеру
|
|
||||||
|
|
||||||
**Ожидаемый результат:** ~340 тестов (34×10), ~15-20 минут параллельно
|
**Тесты на пользователя (8 шт):**
|
||||||
|
U-1: ensure namespace | U-2: create service | U-3: upload code
|
||||||
|
U-4: wait Ready | U-5: invoke1 (count≥1) | U-6: invoke2 (count grew)
|
||||||
|
U-7: isolation (other ns → 404) | U-8: delete
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Executable
+354
@@ -0,0 +1,354 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 2026-03-22 — G_MULTIUSER: 10 параллельных пользователей с Postgres
|
||||||
|
#
|
||||||
|
# Проверяет:
|
||||||
|
# - Namespace isolation при параллельной работе 10 пользователей
|
||||||
|
# - Реальное PG подключение из функции (INSERT/SELECT через env_vars)
|
||||||
|
# - Fake JWT (sub уникален, подпись не проверяется оператором — auth.go)
|
||||||
|
# - /v1/namespaces/{ns}/ensure для создания namespace
|
||||||
|
#
|
||||||
|
# Технические детали:
|
||||||
|
# - 10 пользователей: ns = sless-mu01..sless-mu10, sub = test-user-01..10
|
||||||
|
# - Каждый: ensure → create service → upload → wait Ready → invoke×2 → isolation → delete
|
||||||
|
# - 8 тестов × 10 пользователей = 80 тестов суммарно
|
||||||
|
# - Параллельный запуск через & + wait, результаты через temp-файлы
|
||||||
|
#
|
||||||
|
# Предусловия:
|
||||||
|
# - PG поднят (examples/POSTGRES/terraform.tfvars)
|
||||||
|
# - Оператор v0.1.51+ запущен в кластере
|
||||||
|
# - sshfs mount / VM доступен
|
||||||
|
|
||||||
|
API_BASE="https://sless.kube5s.ru/v1"
|
||||||
|
FN_BASE="https://sless.kube5s.ru/fn"
|
||||||
|
PG_HOST="postgresqlk8s-master.bba55e0f-4c51-4312-8ad3-efc557de331d.svc.cluster.local"
|
||||||
|
PG_USER="user0"
|
||||||
|
PG_PASS="iuEHVxxIPfdFr2kWWmSfNJbgCmDzuEC1rQy38aCNj0rYF23BHguhtKFUAioR1Z4F"
|
||||||
|
PG_DB="db0"
|
||||||
|
NUM_USERS=10
|
||||||
|
RESULTS_DIR=$(mktemp -d)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# make_fake_jwt sub — генерирует JWT с нужным sub.
|
||||||
|
# Подпись фейковая — auth.go проверяет только структуру, sub и exp.
|
||||||
|
# Почему фейковый: nubes JWKS недоступен внутри кластера (см. auth.go).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
make_fake_jwt() {
|
||||||
|
local sub="$1"
|
||||||
|
local exp=$(( $(date +%s) + 86400 ))
|
||||||
|
local iat=$(date +%s)
|
||||||
|
local header='{"alg":"RS256","typ":"JWT"}'
|
||||||
|
local payload
|
||||||
|
payload=$(printf \
|
||||||
|
'{"iss":"auth-api","sub":"%s","exp":%d,"iat":%d,"jti":"fake-%s","auth_time":0,"typ":"","azp":"","session_state":"","acr":"","allowed-origins":null,"realm_access":{"roles":null},"resource_access":{"account":{"roles":null}},"scope":"","sid":"","email_verified":false,"name":"","ClientID":"","company_id":"","groups":null,"preferred_username":"","given_name":"","family_name":"","email":"multitest@example.com"}' \
|
||||||
|
"$sub" "$exp" "$iat" "$sub")
|
||||||
|
local h p
|
||||||
|
h=$(printf '%s' "$header" | base64 -w0 | tr '+/' '-_' | tr -d '=')
|
||||||
|
p=$(printf '%s' "$payload" | base64 -w0 | tr '+/' '-_' | tr -d '=')
|
||||||
|
printf '%s.%s.fakesig_multiuser_test' "$h" "$p"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# make_py_zip zipfile — создаёт zip с Python-функцией + requirements.txt.
|
||||||
|
# Функция: CREATE TABLE IF NOT EXISTS → INSERT → SELECT COUNT(*) → return.
|
||||||
|
# Таблица приходит через env TEST_TABLE (изоляция по namespace).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
make_py_zip() {
|
||||||
|
local zipfile="$1"
|
||||||
|
local tmpdir
|
||||||
|
tmpdir=$(mktemp -d)
|
||||||
|
# Одинарные кавычки в EOF — чтобы bash не раскрывал $ внутри heredoc
|
||||||
|
cat > "$tmpdir/pg_tester.py" << 'PYEOF'
|
||||||
|
# pg_tester.py — тестовая функция: INSERT/SELECT в PG через env_vars.
|
||||||
|
# Entrypoint: pg_tester.handle
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
def handle(event):
|
||||||
|
table = os.environ.get("TEST_TABLE", "test_default")
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=os.environ["PGHOST"],
|
||||||
|
dbname=os.environ["PGDATABASE"],
|
||||||
|
user=os.environ["PGUSER"],
|
||||||
|
password=os.environ["PGPASSWORD"],
|
||||||
|
sslmode="require",
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
# Безопасно: table = hardcoded test name из env_vars, не user input
|
||||||
|
cur.execute(
|
||||||
|
f"CREATE TABLE IF NOT EXISTS {table} "
|
||||||
|
"(id SERIAL PRIMARY KEY, ns TEXT, ts TIMESTAMPTZ DEFAULT now())"
|
||||||
|
)
|
||||||
|
cur.execute(f"INSERT INTO {table} (ns) VALUES (%s)", (table,))
|
||||||
|
conn.commit()
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM {table}")
|
||||||
|
count = cur.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return {"ok": True, "table": table, "count": int(count)}
|
||||||
|
PYEOF
|
||||||
|
printf 'psycopg2-binary==2.9.9\n' > "$tmpdir/requirements.txt"
|
||||||
|
(cd "$tmpdir" && zip -q "$zipfile" pg_tester.py requirements.txt)
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# wait_phase_fn url token [phase] [timeout] — ждёт нужной фазы CRD.
|
||||||
|
# Возвращает 0 если фаза достигнута, 1 если timeout.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
wait_phase_fn() {
|
||||||
|
local url="$1" token="$2" want="${3:-Ready}" timeout="${4:-900}"
|
||||||
|
local elapsed=0 phase
|
||||||
|
while [[ $elapsed -lt $timeout ]]; do
|
||||||
|
phase=$(curl -s "$url" -H "Authorization: Bearer $token" \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('phase',''))" 2>/dev/null)
|
||||||
|
[[ "$phase" == "$want" ]] && return 0
|
||||||
|
sleep 10
|
||||||
|
elapsed=$(( elapsed + 10 ))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# invoke_fn url token [retries] [delay] — вызывает функцию с retry.
|
||||||
|
# Печатает тело ответа. Считает успехом ответ содержащий "ok":true.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
invoke_fn() {
|
||||||
|
local url="$1" token="$2" retries="${3:-5}" delay="${4:-15}"
|
||||||
|
local body ok
|
||||||
|
for i in $(seq 1 "$retries"); do
|
||||||
|
body=$(curl -s -X POST "$url" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}')
|
||||||
|
ok=$(printf '%s' "$body" \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('ok',''))" 2>/dev/null)
|
||||||
|
if [[ "$ok" == "True" ]]; then
|
||||||
|
printf '%s' "$body"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[[ $i -lt $retries ]] && sleep "$delay"
|
||||||
|
done
|
||||||
|
printf '%s' "$body"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# run_user_test N — полный lifecycle теста для пользователя N.
|
||||||
|
# Результат: "pass fail" записывается в $RESULTS_DIR/user_N.result
|
||||||
|
# Запускается в subshell, stdout → $RESULTS_DIR/user_N.log
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
run_user_test() {
|
||||||
|
local n="$1"
|
||||||
|
local ns="sless-mu$(printf '%02d' "$n")"
|
||||||
|
local sub="test-user-$(printf '%02d' "$n")"
|
||||||
|
local token
|
||||||
|
token=$(make_fake_jwt "$sub")
|
||||||
|
local table="test_sless_mu$(printf '%02d' "$n")"
|
||||||
|
local svc="pg-mu$(printf '%02d' "$n")"
|
||||||
|
local pass=0 fail=0
|
||||||
|
local code body c1 c2
|
||||||
|
|
||||||
|
echo "=== User $n ns=$ns svc=$svc table=$table ==="
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-1: Ensure namespace
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
"$API_BASE/namespaces/${ns}/ensure" \
|
||||||
|
-H "Authorization: Bearer $token")
|
||||||
|
if [[ "$code" =~ ^(200|201)$ ]]; then
|
||||||
|
echo "U${n}-1 PASS ensure($code)"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-1 FAIL ensure=$code"
|
||||||
|
(( fail++ ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-2: Create service с PG env_vars
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
local create_json
|
||||||
|
create_json=$(printf \
|
||||||
|
'{"name":"%s","runtime":"python3.11","entrypoint":"pg_tester.handle","env_vars":{"PGHOST":"%s","PGDATABASE":"%s","PGUSER":"%s","PGPASSWORD":"%s","TEST_TABLE":"%s"}}' \
|
||||||
|
"$svc" "$PG_HOST" "$PG_DB" "$PG_USER" "$PG_PASS" "$table")
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
"$API_BASE/namespaces/${ns}/services" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$create_json")
|
||||||
|
if [[ "$code" == "201" ]]; then
|
||||||
|
echo "U${n}-2 PASS create($code)"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-2 FAIL create=$code"
|
||||||
|
(( fail++ ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-3: Upload code (zip с pg_tester.py + requirements.txt)
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
local zipf
|
||||||
|
zipf=$(mktemp --suffix=.zip)
|
||||||
|
make_py_zip "$zipf"
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
"$API_BASE/namespaces/${ns}/services/${svc}/upload" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-F "file=@${zipf}")
|
||||||
|
rm -f "$zipf"
|
||||||
|
if [[ "$code" == "200" ]]; then
|
||||||
|
echo "U${n}-3 PASS upload($code)"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-3 FAIL upload=$code"
|
||||||
|
(( fail++ ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-4: Wait for Ready (Kaniko + pod startup; psycopg2-binary тяжёлый)
|
||||||
|
# Timeout 900s — увеличен относительно G20 (там без pip install)
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
echo "U${n}: ожидаем Ready (max 900s)..."
|
||||||
|
sleep 15
|
||||||
|
if wait_phase_fn "$API_BASE/namespaces/${ns}/services/${svc}" "$token" "Ready" 900; then
|
||||||
|
echo "U${n}-4 PASS Ready"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-4 FAIL не стал Ready за 900s"
|
||||||
|
(( fail++ ))
|
||||||
|
printf '%d %d\n' "$pass" "$fail" > "$RESULTS_DIR/user_${n}.result"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-5: Первый invoke — функция должна создать таблицу и вставить строку
|
||||||
|
# sleep 20: pod startup race (known limitation: Ready до pod-ready)
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
sleep 20
|
||||||
|
body=$(invoke_fn "$FN_BASE/${ns}/${svc}" "$token" 5 15)
|
||||||
|
c1=$(printf '%s' "$body" \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('count',0))" 2>/dev/null)
|
||||||
|
if [[ "$c1" =~ ^[0-9]+$ && "$c1" -ge 1 ]]; then
|
||||||
|
echo "U${n}-5 PASS invoke1 count=$c1"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-5 FAIL invoke1 resp=$body"
|
||||||
|
(( fail++ ))
|
||||||
|
c1=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-6: Второй invoke — count должен вырасти (ещё один INSERT)
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
sleep 3
|
||||||
|
body=$(invoke_fn "$FN_BASE/${ns}/${svc}" "$token" 3 10)
|
||||||
|
c2=$(printf '%s' "$body" \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('count',0))" 2>/dev/null)
|
||||||
|
if [[ "$c2" =~ ^[0-9]+$ && "$c2" -gt "$c1" ]]; then
|
||||||
|
echo "U${n}-6 PASS invoke2 count $c1→$c2"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-6 FAIL invoke2 c1=$c1 c2=$c2 resp=$body"
|
||||||
|
(( fail++ ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-7: Isolation — сервис svc видим ТОЛЬКО в своём ns, не в соседнем.
|
||||||
|
# Проверяем: GET своего сервиса в namespace следующего пользователя → 404
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
local other_n=$(( (n % NUM_USERS) + 1 ))
|
||||||
|
local other_ns="sless-mu$(printf '%02d' "$other_n")"
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X GET \
|
||||||
|
"$API_BASE/namespaces/${other_ns}/services/${svc}" \
|
||||||
|
-H "Authorization: Bearer $token")
|
||||||
|
if [[ "$code" == "404" ]]; then
|
||||||
|
echo "U${n}-7 PASS isolation: ${svc} в ${other_ns} → 404"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-7 FAIL isolation: ${svc} в ${other_ns} → $code (ожидали 404)"
|
||||||
|
(( fail++ ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# U-8: Delete service — cleanup
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
"$API_BASE/namespaces/${ns}/services/${svc}" \
|
||||||
|
-H "Authorization: Bearer $token")
|
||||||
|
if [[ "$code" == "204" ]]; then
|
||||||
|
echo "U${n}-8 PASS delete($code)"
|
||||||
|
(( pass++ ))
|
||||||
|
else
|
||||||
|
echo "U${n}-8 FAIL delete=$code"
|
||||||
|
(( fail++ ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== User $n DONE: pass=$pass fail=$fail ==="
|
||||||
|
printf '%d %d\n' "$pass" "$fail" > "$RESULTS_DIR/user_${n}.result"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# MAIN
|
||||||
|
# ===========================================================================
|
||||||
|
echo "============================================================"
|
||||||
|
echo "G_MULTIUSER: $NUM_USERS параллельных пользователей с Postgres"
|
||||||
|
echo "============================================================"
|
||||||
|
date "+%Y-%m-%d %H:%M:%S"
|
||||||
|
echo "API: $API_BASE"
|
||||||
|
echo "PG: $PG_HOST / $PG_DB"
|
||||||
|
echo "Тестов: $NUM_USERS × 8 = $((NUM_USERS * 8))"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Запускаем всех пользователей параллельно
|
||||||
|
for i in $(seq 1 $NUM_USERS); do
|
||||||
|
run_user_test "$i" > "$RESULTS_DIR/user_${i}.log" 2>&1 &
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Запущено $NUM_USERS параллельных пользователей. Ждём завершения..."
|
||||||
|
wait
|
||||||
|
echo "Все завершились."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Сбор результатов
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
total_pass=0
|
||||||
|
total_fail=0
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Результаты по пользователям:"
|
||||||
|
echo "============================================================"
|
||||||
|
for i in $(seq 1 $NUM_USERS); do
|
||||||
|
resf="$RESULTS_DIR/user_${i}.result"
|
||||||
|
logf="$RESULTS_DIR/user_${i}.log"
|
||||||
|
if [[ -f "$resf" ]]; then
|
||||||
|
read -r p f < "$resf"
|
||||||
|
total_pass=$(( total_pass + p ))
|
||||||
|
total_fail=$(( total_fail + f ))
|
||||||
|
echo " User $i (sless-mu$(printf '%02d' "$i")): $p PASS / $f FAIL"
|
||||||
|
else
|
||||||
|
echo " User $i: НЕТ РЕЗУЛЬТАТА (timeout или crash)"
|
||||||
|
total_fail=$(( total_fail + 8 ))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "ИТОГО: $total_pass PASS / $total_fail FAIL"
|
||||||
|
echo "(ожидалось: $((NUM_USERS * 8)) тестов)"
|
||||||
|
if [[ $total_fail -eq 0 ]]; then
|
||||||
|
echo "РЕЗУЛЬТАТ: PASS ✓"
|
||||||
|
else
|
||||||
|
echo "РЕЗУЛЬТАТ: FAIL ✗"
|
||||||
|
fi
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Детальные логи каждого пользователя
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "=== Детальные логи ==="
|
||||||
|
for i in $(seq 1 $NUM_USERS); do
|
||||||
|
echo ""
|
||||||
|
echo "--- User $i (sless-mu$(printf '%02d' "$i")) ---"
|
||||||
|
cat "$RESULTS_DIR/user_${i}.log" 2>/dev/null || echo "(нет лога)"
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -rf "$RESULTS_DIR"
|
||||||
Reference in New Issue
Block a user