test(ldd): конкурентность, нагрузка и release gate
This commit is contained in:
31
scripts/ldd-release.sh
Executable file
31
scripts/ldd-release.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Полный LDD release gate: тесты, линтеры, upgrade, mutation, runtime и browser E2E.
|
||||
# Требует Docker, Java 21, Node.js/npm и установленный Chromium Playwright.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
log() { echo; echo ">> $*"; }
|
||||
|
||||
log "1/6 Тесты, линтеры и evidence gate"
|
||||
./gradlew test ktlintCheck detekt evidenceCheck --console=plain
|
||||
|
||||
log "2/6 Upgrade с master-схемы"
|
||||
./scripts/ldd-migration-upgrade.sh
|
||||
|
||||
log "3/6 Mutation testing"
|
||||
./scripts/ldd-mutation.sh
|
||||
|
||||
log "4/6 Runtime smoke"
|
||||
./scripts/ldd-runtime.sh
|
||||
|
||||
log "5/6 Browser E2E"
|
||||
./scripts/ldd-browser.sh
|
||||
|
||||
log "6/6 Финальный evidence gate"
|
||||
./gradlew evidenceCheck --console=plain
|
||||
|
||||
echo
|
||||
echo "LDD release evidence готов:"
|
||||
echo " build/evidence/"
|
||||
echo " build/reports/evidence/index.md"
|
||||
@@ -78,8 +78,11 @@ mkdir -p "$(dirname "$EVIDENCE")"
|
||||
|
||||
log "Проверяю HTTP-сценарии..."
|
||||
python3 - "$EVIDENCE" <<'PY'
|
||||
import concurrent.futures
|
||||
import json
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
@@ -216,6 +219,22 @@ try:
|
||||
token=admin_token,
|
||||
detail="create lab",
|
||||
)
|
||||
student = expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
gateway + "/api/v1/groups/" + group["id"] + "/students",
|
||||
200,
|
||||
data={
|
||||
"studentNo": "ldd-student-" + suffix,
|
||||
"lastName": "Тестов",
|
||||
"firstName": "Студент",
|
||||
"middleName": "",
|
||||
"basis": "бюджет",
|
||||
"note": "",
|
||||
},
|
||||
token=admin_token,
|
||||
detail="create student",
|
||||
)
|
||||
teacher_token = expect(
|
||||
"runtime.course_flow",
|
||||
"POST",
|
||||
@@ -283,6 +302,44 @@ try:
|
||||
raise AssertionError("audit actions")
|
||||
record("runtime.audit_visible", "passed", f"аудит содержит {sorted(required)}")
|
||||
|
||||
# 5b. Нагрузочный smoke чтения: курс, лабы, профиль и аудит.
|
||||
load_paths = [
|
||||
(gateway + "/api/v1/courses/" + course["id"], teacher_token),
|
||||
(gateway + "/api/v1/labs?courseId=" + course["id"], teacher_token),
|
||||
(gateway + "/api/v1/courses/" + course["id"] + "/students/" + student["id"] + "/profile", teacher_token),
|
||||
(gateway + "/api/v1/audit?limit=100", admin_token),
|
||||
]
|
||||
|
||||
def timed_get(path, token):
|
||||
started = time.perf_counter()
|
||||
status, _ = req("GET", path, token=token)
|
||||
return status, time.perf_counter() - started
|
||||
|
||||
latencies = []
|
||||
errors = 0
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
|
||||
futures = [
|
||||
pool.submit(timed_get, path, token)
|
||||
for _ in range(25)
|
||||
for path, token in load_paths
|
||||
]
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
status, latency = future.result()
|
||||
latencies.append(latency)
|
||||
if status != 200:
|
||||
errors += 1
|
||||
latencies.sort()
|
||||
p95 = latencies[min(len(latencies) - 1, int(len(latencies) * 0.95))]
|
||||
average = statistics.mean(latencies)
|
||||
load_ok = errors == 0 and p95 < 2.0
|
||||
record(
|
||||
"runtime.load_gradebook_profile",
|
||||
"passed" if load_ok else "failed",
|
||||
f"requests={len(latencies)}, errors={errors}, avg={average:.3f}s, p95={p95:.3f}s",
|
||||
)
|
||||
if not load_ok:
|
||||
raise AssertionError("load smoke")
|
||||
|
||||
# 6. Web-страница входа и публичное дерево.
|
||||
status, _ = req("GET", web + "/login")
|
||||
if status != 200:
|
||||
|
||||
Reference in New Issue
Block a user