557 lines
19 KiB
Bash
Executable File
557 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# BukidBounty Performance & Load Test Script
|
||
#
|
||
# Usage:
|
||
# ./scripts/perf-test.sh [options] [test...]
|
||
#
|
||
# Options:
|
||
# -u, --url URL Base URL (default: http://localhost:9522)
|
||
# -t, --token TOKEN X-Perf-Token value (default: $PERF_API_TOKEN env)
|
||
# -s, --store HASH Store hashkey for POS simulation (required for pos/*)
|
||
# -c, --concurrency N Max concurrent workers for ramp test (default: 50)
|
||
# -o, --output FILE Write JSON summary to FILE
|
||
# --no-color Disable colored output
|
||
#
|
||
# Tests (pass one or more; default: all):
|
||
# ping Verify connectivity and token
|
||
# seed Create 50 users + 20 stores + 100 products, report timing
|
||
# pos POS simulation: 1 → 5 → 20 items × 20 cycles
|
||
# concurrent Hit /api/perf/pos/simulate with N parallel workers
|
||
# ramp Ramp concurrency from 1 → MAX workers, report throughput
|
||
# web Curl-based throughput on public endpoints (no token needed)
|
||
#
|
||
# Environment:
|
||
# PERF_API_TOKEN Token set in .env on the server (required)
|
||
# PERF_BASE_URL Overrides --url
|
||
# PERF_STORE_HASH Overrides --store
|
||
#
|
||
# Examples:
|
||
# # Quick health check (local dev):
|
||
# PERF_API_TOKEN=secret ./scripts/perf-test.sh ping
|
||
#
|
||
# # Full suite against prod:
|
||
# ./scripts/perf-test.sh -u https://yourapp.com -t SECRET -s STORE_HASH
|
||
#
|
||
# # Concurrent POS with 30 workers:
|
||
# ./scripts/perf-test.sh -u https://yourapp.com -t SECRET -s STORE_HASH \
|
||
# -c 30 concurrent
|
||
#
|
||
# # Ramp test only, save JSON report:
|
||
# ./scripts/perf-test.sh ... ramp -o results.json
|
||
# =============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Defaults / env
|
||
# ---------------------------------------------------------------------------
|
||
BASE_URL="${PERF_BASE_URL:-http://localhost:9522}"
|
||
TOKEN="${PERF_API_TOKEN:-}"
|
||
STORE_HASH="${PERF_STORE_HASH:-}"
|
||
MAX_CONCURRENCY=50
|
||
OUTPUT_FILE=""
|
||
USE_COLOR=true
|
||
RUN_TESTS=()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Arg parsing
|
||
# ---------------------------------------------------------------------------
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-u|--url) BASE_URL="$2"; shift 2 ;;
|
||
-t|--token) TOKEN="$2"; shift 2 ;;
|
||
-s|--store) STORE_HASH="$2"; shift 2 ;;
|
||
-c|--concurrency) MAX_CONCURRENCY="$2"; shift 2 ;;
|
||
-o|--output) OUTPUT_FILE="$2"; shift 2 ;;
|
||
--no-color) USE_COLOR=false; shift ;;
|
||
ping|seed|pos|concurrent|ramp|web)
|
||
RUN_TESTS+=("$1"); shift ;;
|
||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
[[ ${#RUN_TESTS[@]} -eq 0 ]] && RUN_TESTS=(ping seed pos concurrent ramp web)
|
||
|
||
BASE_URL="${BASE_URL%/}" # strip trailing slash
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Colors
|
||
# ---------------------------------------------------------------------------
|
||
if $USE_COLOR && [[ -t 1 ]]; then
|
||
C_RESET="\033[0m"; C_BOLD="\033[1m"
|
||
C_GREEN="\033[32m"; C_YELLOW="\033[33m"; C_RED="\033[31m"
|
||
C_CYAN="\033[36m"; C_BLUE="\033[34m"; C_DIM="\033[2m"
|
||
else
|
||
C_RESET=""; C_BOLD=""; C_GREEN=""; C_YELLOW=""; C_RED=""
|
||
C_CYAN=""; C_BLUE=""; C_DIM=""
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
NOW() { date +"%Y-%m-%d %H:%M:%S"; }
|
||
log_section() { echo -e "\n${C_BOLD}${C_BLUE}══ $1 ══${C_RESET}"; }
|
||
log_ok() { echo -e " ${C_GREEN}✔${C_RESET} $*"; }
|
||
log_warn() { echo -e " ${C_YELLOW}⚠${C_RESET} $*"; }
|
||
log_err() { echo -e " ${C_RED}✖${C_RESET} $*"; }
|
||
log_dim() { echo -e " ${C_DIM}$*${C_RESET}"; }
|
||
|
||
# Check required tools
|
||
need() { command -v "$1" &>/dev/null || { log_err "Required tool not found: $1 — install it first"; exit 1; }; }
|
||
need curl
|
||
need jq
|
||
need bc
|
||
need awk
|
||
|
||
# curl wrapper: returns HTTP body; exits 1 on HTTP error or curl failure
|
||
# Usage: perf_get PATH
|
||
perf_get() {
|
||
local path="$1"
|
||
curl -sf --max-time 30 \
|
||
-H "X-Perf-Token: ${TOKEN}" \
|
||
-H "Accept: application/json" \
|
||
"${BASE_URL}${path}"
|
||
}
|
||
|
||
# POST JSON body
|
||
perf_post() {
|
||
local path="$1"
|
||
local body="$2"
|
||
curl -sf --max-time 60 \
|
||
-H "X-Perf-Token: ${TOKEN}" \
|
||
-H "Content-Type: application/json" \
|
||
-H "Accept: application/json" \
|
||
-d "$body" \
|
||
"${BASE_URL}${path}"
|
||
}
|
||
|
||
# POST silently into a tmp file; echo that file path
|
||
perf_post_async() {
|
||
local path="$1" body="$2" out="$3"
|
||
curl -s --max-time 60 \
|
||
-H "X-Perf-Token: ${TOKEN}" \
|
||
-H "Content-Type: application/json" \
|
||
-H "Accept: application/json" \
|
||
-d "$body" \
|
||
"${BASE_URL}${path}" -o "$out" 2>/dev/null
|
||
}
|
||
|
||
# Extract a numeric field from JSON
|
||
jnum() { echo "$1" | jq -r ".$2 // 0"; }
|
||
|
||
# Pretty-print a ms value
|
||
ms_label() {
|
||
local v
|
||
v=$(printf "%.1f" "${1:-0}")
|
||
if (( $(echo "$v < 100" | bc -l) )); then
|
||
echo -e "${C_GREEN}${v}ms${C_RESET}"
|
||
elif (( $(echo "$v < 500" | bc -l) )); then
|
||
echo -e "${C_YELLOW}${v}ms${C_RESET}"
|
||
else
|
||
echo -e "${C_RED}${v}ms${C_RESET}"
|
||
fi
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# JSON results accumulator
|
||
# ---------------------------------------------------------------------------
|
||
RESULTS='{"run_at":"","tests":{}}'
|
||
result_set() {
|
||
# result_set <test> <key> <value(json)>
|
||
RESULTS=$(echo "$RESULTS" | jq --arg t "$1" --arg k "$2" --argjson v "$3" \
|
||
'.tests[$t][$k] = $v')
|
||
}
|
||
result_section() {
|
||
RESULTS=$(echo "$RESULTS" | jq --arg t "$1" '.tests[$t] //= {}')
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Guard: token must be set for API tests
|
||
# ---------------------------------------------------------------------------
|
||
require_token() {
|
||
if [[ -z "$TOKEN" ]]; then
|
||
log_err "PERF_API_TOKEN / --token is required for this test"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
require_store() {
|
||
if [[ -z "$STORE_HASH" ]]; then
|
||
log_err "--store / PERF_STORE_HASH is required for this test"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# TEST: ping
|
||
# =============================================================================
|
||
test_ping() {
|
||
log_section "PING — Connectivity & token check"
|
||
require_token
|
||
result_section "ping"
|
||
|
||
local t0 body
|
||
t0=$(date +%s%N)
|
||
if ! body=$(perf_get "/api/perf/ping" 2>&1); then
|
||
log_err "Ping failed: $body"
|
||
result_set ping ok false
|
||
return 1
|
||
fi
|
||
local elapsed_ms
|
||
elapsed_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))
|
||
|
||
local ts php
|
||
ts=$(echo "$body" | jq -r '.ts // "?"')
|
||
php=$(echo "$body" | jq -r '.php // "?"')
|
||
|
||
log_ok "Server responded in $(ms_label $elapsed_ms)"
|
||
log_ok "Server time: ${ts} PHP: ${php}"
|
||
log_dim "Target: ${BASE_URL}"
|
||
|
||
result_set ping ok true
|
||
result_set ping latency_ms "$elapsed_ms"
|
||
result_set ping server_time "\"$ts\""
|
||
}
|
||
|
||
# =============================================================================
|
||
# TEST: seed
|
||
# =============================================================================
|
||
test_seed() {
|
||
log_section "SEED — Create synthetic users / stores / products"
|
||
require_token
|
||
result_section "seed"
|
||
|
||
# 1. Seed users
|
||
local r
|
||
echo -e " Seeding 50 users..."
|
||
r=$(perf_post "/api/perf/seed/users" '{"count":50,"prefix":"loadtest"}')
|
||
local u_ms u_avg
|
||
u_ms=$(jnum "$r" total_ms)
|
||
u_avg=$(jnum "$r" avg_ms)
|
||
log_ok "Users — 50 created total: $(ms_label $u_ms) avg/user: $(ms_label $u_avg)"
|
||
result_set seed users_total_ms "$u_ms"
|
||
result_set seed users_avg_ms "$u_avg"
|
||
|
||
# 2. Seed stores
|
||
echo -e " Seeding 20 stores..."
|
||
r=$(perf_post "/api/perf/seed/stores" '{"count":20,"prefix":"LoadTestStore"}')
|
||
local s_ms s_avg
|
||
s_ms=$(jnum "$r" total_ms)
|
||
s_avg=$(jnum "$r" avg_ms)
|
||
log_ok "Stores — 20 created total: $(ms_label $s_ms) avg/store: $(ms_label $s_avg)"
|
||
result_set seed stores_total_ms "$s_ms"
|
||
result_set seed stores_avg_ms "$s_avg"
|
||
|
||
# 3. Seed products (global, no store attach)
|
||
echo -e " Seeding 100 products..."
|
||
r=$(perf_post "/api/perf/seed/products" '{"count":100,"prefix":"LoadTestPrd","attach_to_store":false}')
|
||
local p_ms p_avg
|
||
p_ms=$(jnum "$r" total_ms)
|
||
p_avg=$(jnum "$r" avg_ms)
|
||
log_ok "Products— 100 created total: $(ms_label $p_ms) avg/prod: $(ms_label $p_avg)"
|
||
result_set seed products_total_ms "$p_ms"
|
||
result_set seed products_avg_ms "$p_avg"
|
||
}
|
||
|
||
# =============================================================================
|
||
# TEST: pos — sequential POS simulation at various item counts
|
||
# =============================================================================
|
||
test_pos() {
|
||
log_section "POS — Sequential simulation (1 / 5 / 20 items × 20 cycles)"
|
||
require_token
|
||
require_store
|
||
result_section "pos"
|
||
|
||
local scenario items cycles
|
||
for scenario in "1items:1:20" "5items:5:20" "20items:20:20"; do
|
||
label="${scenario%%:*}"
|
||
rest="${scenario#*:}"
|
||
items="${rest%%:*}"
|
||
cycles="${rest#*:}"
|
||
|
||
echo -e " Running ${cycles} cycles × ${items} items..."
|
||
local r avg_ms min_ms max_ms
|
||
r=$(perf_post "/api/perf/pos/simulate" \
|
||
"{\"store_hash\":\"${STORE_HASH}\",\"items\":${items},\"cycles\":${cycles},\"complete\":true}")
|
||
|
||
avg_ms=$(jnum "$r" avg_cycle_ms)
|
||
min_ms=$(jnum "$r" min_cycle_ms)
|
||
max_ms=$(jnum "$r" max_cycle_ms)
|
||
|
||
log_ok "${label} avg: $(ms_label $avg_ms) min: $(ms_label $min_ms) max: $(ms_label $max_ms)"
|
||
result_set pos "${label}_avg_ms" "$avg_ms"
|
||
result_set pos "${label}_min_ms" "$min_ms"
|
||
result_set pos "${label}_max_ms" "$max_ms"
|
||
done
|
||
}
|
||
|
||
# =============================================================================
|
||
# TEST: concurrent — N parallel POS simulations at once
|
||
# =============================================================================
|
||
test_concurrent() {
|
||
log_section "CONCURRENT — Parallel POS workers (5 items / session)"
|
||
require_token
|
||
require_store
|
||
result_section "concurrent"
|
||
|
||
local workers_list=(1 5 10 25 $MAX_CONCURRENCY)
|
||
# deduplicate and sort
|
||
workers_list=($(printf '%s\n' "${workers_list[@]}" | sort -nu))
|
||
|
||
local TMPDIR_PERF
|
||
TMPDIR_PERF=$(mktemp -d)
|
||
trap "rm -rf '$TMPDIR_PERF'" EXIT
|
||
|
||
for workers in "${workers_list[@]}"; do
|
||
[[ $workers -gt $MAX_CONCURRENCY ]] && continue
|
||
|
||
local pids=() out_files=()
|
||
local t0
|
||
t0=$(date +%s%N)
|
||
|
||
for ((w=1; w<=workers; w++)); do
|
||
local out="${TMPDIR_PERF}/w${workers}_${w}.json"
|
||
out_files+=("$out")
|
||
perf_post_async "/api/perf/pos/simulate" \
|
||
"{\"store_hash\":\"${STORE_HASH}\",\"items\":5,\"cycles\":3,\"complete\":true}" \
|
||
"$out" &
|
||
pids+=($!)
|
||
done
|
||
|
||
# Wait for all workers
|
||
local failed=0
|
||
for pid in "${pids[@]}"; do
|
||
wait "$pid" 2>/dev/null || (( failed++ )) || true
|
||
done
|
||
|
||
local wall_ms errors=0 total_avg=0 count=0
|
||
wall_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))
|
||
|
||
for f in "${out_files[@]}"; do
|
||
if [[ ! -f "$f" ]]; then (( errors++ )); continue; fi
|
||
local ok avg
|
||
ok=$(jq -r '.success // false' "$f" 2>/dev/null)
|
||
if [[ "$ok" != "true" ]]; then (( errors++ )); continue; fi
|
||
avg=$(jq -r '.avg_cycle_ms // 0' "$f" 2>/dev/null)
|
||
total_avg=$(echo "$total_avg + $avg" | bc)
|
||
(( count++ ))
|
||
done
|
||
|
||
local mean_avg=0
|
||
[[ $count -gt 0 ]] && mean_avg=$(echo "scale=1; $total_avg / $count" | bc)
|
||
|
||
local status_icon="${C_GREEN}✔${C_RESET}"
|
||
[[ $errors -gt 0 ]] && status_icon="${C_RED}✖${C_RESET}"
|
||
|
||
printf " ${status_icon} %3d workers wall: $(ms_label $wall_ms) mean cycle: $(ms_label $mean_avg) errors: %d\n" \
|
||
"$workers" "$errors"
|
||
|
||
result_set concurrent "w${workers}_wall_ms" "$wall_ms"
|
||
result_set concurrent "w${workers}_mean_cycle_ms" "$mean_avg"
|
||
result_set concurrent "w${workers}_errors" "$errors"
|
||
done
|
||
|
||
rm -rf "$TMPDIR_PERF"
|
||
trap - EXIT
|
||
}
|
||
|
||
# =============================================================================
|
||
# TEST: ramp — gradually increase concurrency to find the saturation point
|
||
# =============================================================================
|
||
test_ramp() {
|
||
log_section "RAMP — Concurrency ramp 1 → ${MAX_CONCURRENCY} workers"
|
||
require_token
|
||
require_store
|
||
result_section "ramp"
|
||
|
||
# Steps: 1, 5, 10, 20, 30, MAX (if not already listed)
|
||
local steps=(1 5 10 20 30)
|
||
steps+=($MAX_CONCURRENCY)
|
||
steps=($(printf '%s\n' "${steps[@]}" | sort -nu | awk -v m=$MAX_CONCURRENCY '$1<=m'))
|
||
|
||
local TMPDIR_RAMP
|
||
TMPDIR_RAMP=$(mktemp -d)
|
||
trap "rm -rf '$TMPDIR_RAMP'" EXIT
|
||
|
||
echo -e " ${C_DIM}workers | wall_ms | mean_cycle_ms | throughput(cyc/s) | errors${C_RESET}"
|
||
echo -e " ${C_DIM}--------|---------|---------------|-----------------|-------${C_RESET}"
|
||
|
||
local prev_throughput=0
|
||
local saturation_workers=""
|
||
|
||
for workers in "${steps[@]}"; do
|
||
local pids=() out_files=()
|
||
local t0
|
||
t0=$(date +%s%N)
|
||
|
||
for ((w=1; w<=workers; w++)); do
|
||
local out="${TMPDIR_RAMP}/ramp_${workers}_${w}.json"
|
||
out_files+=("$out")
|
||
perf_post_async "/api/perf/pos/simulate" \
|
||
"{\"store_hash\":\"${STORE_HASH}\",\"items\":5,\"cycles\":5,\"complete\":true}" \
|
||
"$out" &
|
||
pids+=($!)
|
||
done
|
||
|
||
for pid in "${pids[@]}"; do wait "$pid" 2>/dev/null || true; done
|
||
|
||
local wall_ms errors=0 total_cycles=0 total_avg=0 count=0
|
||
wall_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))
|
||
|
||
for f in "${out_files[@]}"; do
|
||
[[ ! -f "$f" ]] && (( errors++ )) && continue
|
||
local ok
|
||
ok=$(jq -r '.success // false' "$f" 2>/dev/null)
|
||
[[ "$ok" != "true" ]] && (( errors++ )) && continue
|
||
local cyc avg
|
||
cyc=$(jq -r '.cycles // 0' "$f" 2>/dev/null)
|
||
avg=$(jq -r '.avg_cycle_ms // 0' "$f" 2>/dev/null)
|
||
total_cycles=$(( total_cycles + cyc ))
|
||
total_avg=$(echo "$total_avg + $avg" | bc)
|
||
(( count++ ))
|
||
done
|
||
|
||
local mean_avg=0 throughput=0
|
||
[[ $count -gt 0 ]] && mean_avg=$(echo "scale=1; $total_avg / $count" | bc)
|
||
[[ $wall_ms -gt 0 ]] && throughput=$(echo "scale=1; $total_cycles * 1000 / $wall_ms" | bc)
|
||
|
||
# Saturation detection: throughput stopped growing by >5%
|
||
if [[ -n "$prev_throughput" ]] && (( $(echo "$prev_throughput > 0" | bc -l) )); then
|
||
local growth
|
||
growth=$(echo "scale=2; ($throughput - $prev_throughput) / $prev_throughput * 100" | bc 2>/dev/null || echo 0)
|
||
if (( $(echo "$growth < 5 && $workers > 1" | bc -l) )) && [[ -z "$saturation_workers" ]]; then
|
||
saturation_workers=$workers
|
||
fi
|
||
fi
|
||
prev_throughput=$throughput
|
||
|
||
local err_col="${C_GREEN}${errors}${C_RESET}"
|
||
[[ $errors -gt 0 ]] && err_col="${C_RED}${errors}${C_RESET}"
|
||
|
||
printf " %7d | %7d | %13.1f | %17.1f | ${err_col}\n" \
|
||
"$workers" "$wall_ms" "$mean_avg" "$throughput"
|
||
|
||
result_set ramp "w${workers}" "{\"wall_ms\":$wall_ms,\"mean_cycle_ms\":$mean_avg,\"throughput\":$throughput,\"errors\":$errors}"
|
||
done
|
||
|
||
if [[ -n "$saturation_workers" ]]; then
|
||
log_warn "Throughput plateaued around ${saturation_workers} workers — saturation point"
|
||
result_set ramp saturation_workers "$saturation_workers"
|
||
else
|
||
log_ok "No saturation detected at ${MAX_CONCURRENCY} workers — system still scaling"
|
||
fi
|
||
|
||
rm -rf "$TMPDIR_RAMP"
|
||
trap - EXIT
|
||
}
|
||
|
||
# =============================================================================
|
||
# TEST: web — curl burst on public/session endpoints (no token needed)
|
||
# =============================================================================
|
||
test_web() {
|
||
log_section "WEB — Public endpoint throughput (curl burst, no token)"
|
||
result_section "web"
|
||
|
||
local endpoints=(
|
||
"GET|/|Public home"
|
||
"GET|/login|Login page"
|
||
"GET|/api/public/landing-page|Public landing page API"
|
||
)
|
||
|
||
local TMPDIR_WEB
|
||
TMPDIR_WEB=$(mktemp -d)
|
||
trap "rm -rf '$TMPDIR_WEB'" EXIT
|
||
|
||
for entry in "${endpoints[@]}"; do
|
||
local method path label
|
||
method="${entry%%|*}"; rest="${entry#*|}"; path="${rest%%|*}"; label="${rest#*|}"
|
||
|
||
# Burst: 20 sequential requests, measure p50/p95/p99
|
||
local times=()
|
||
local errors=0
|
||
for i in $(seq 1 20); do
|
||
local t0 t1 ms
|
||
t0=$(date +%s%N)
|
||
local http_code
|
||
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
|
||
-X "$method" "${BASE_URL}${path}" 2>/dev/null || echo "000")
|
||
t1=$(date +%s%N)
|
||
ms=$(( ( t1 - t0 ) / 1000000 ))
|
||
if [[ "$http_code" == "000" || "$http_code" == "5"* ]]; then
|
||
(( errors++ ))
|
||
else
|
||
times+=($ms)
|
||
fi
|
||
done
|
||
|
||
if [[ ${#times[@]} -eq 0 ]]; then
|
||
log_err "${label} — all 20 requests failed"
|
||
result_set web "$path" '{"errors":20}'
|
||
continue
|
||
fi
|
||
|
||
# Sort times
|
||
local sorted
|
||
sorted=($(printf '%s\n' "${times[@]}" | sort -n))
|
||
local n=${#sorted[@]}
|
||
local p50_idx p95_idx p99_idx
|
||
p50_idx=$(( (n * 50) / 100 ))
|
||
p95_idx=$(( (n * 95) / 100 ))
|
||
p99_idx=$(( (n * 99) / 100 ))
|
||
[[ $p50_idx -ge $n ]] && p50_idx=$(( n - 1 ))
|
||
[[ $p95_idx -ge $n ]] && p95_idx=$(( n - 1 ))
|
||
[[ $p99_idx -ge $n ]] && p99_idx=$(( n - 1 ))
|
||
|
||
local p50=${sorted[$p50_idx]}
|
||
local p95=${sorted[$p95_idx]}
|
||
local p99=${sorted[$p99_idx]}
|
||
local avg
|
||
avg=$(printf '%s\n' "${times[@]}" | awk '{sum+=$1} END {printf "%.0f", sum/NR}')
|
||
|
||
log_ok "${label}"
|
||
log_dim " avg: $(ms_label $avg) p50: $(ms_label $p50) p95: $(ms_label $p95) p99: $(ms_label $p99) errors: $errors / 20"
|
||
|
||
result_set web "$path" "{\"avg_ms\":$avg,\"p50_ms\":$p50,\"p95_ms\":$p95,\"p99_ms\":$p99,\"errors\":$errors}"
|
||
done
|
||
|
||
rm -rf "$TMPDIR_WEB"
|
||
trap - EXIT
|
||
}
|
||
|
||
# =============================================================================
|
||
# MAIN
|
||
# =============================================================================
|
||
echo -e "\n${C_BOLD}${C_CYAN}BukidBounty Performance Test${C_RESET} $(NOW)"
|
||
echo -e " Target : ${C_BOLD}${BASE_URL}${C_RESET}"
|
||
echo -e " Tests : ${RUN_TESTS[*]}"
|
||
[[ -n "$STORE_HASH" ]] && echo -e " Store : ${STORE_HASH}"
|
||
|
||
RESULTS=$(echo "$RESULTS" | jq --arg d "$(NOW)" '.run_at = $d')
|
||
|
||
for t in "${RUN_TESTS[@]}"; do
|
||
case "$t" in
|
||
ping) test_ping || true ;;
|
||
seed) test_seed || true ;;
|
||
pos) test_pos || true ;;
|
||
concurrent) test_concurrent || true ;;
|
||
ramp) test_ramp || true ;;
|
||
web) test_web || true ;;
|
||
*) log_warn "Unknown test: $t (skipped)" ;;
|
||
esac
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Summary
|
||
# ---------------------------------------------------------------------------
|
||
log_section "SUMMARY"
|
||
RESULTS=$(echo "$RESULTS" | jq --arg url "$BASE_URL" '.target = $url')
|
||
|
||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||
echo "$RESULTS" | jq . > "$OUTPUT_FILE"
|
||
log_ok "Results written to: ${OUTPUT_FILE}"
|
||
fi
|
||
|
||
echo -e "\n${C_DIM}Tip: run with -o results.json to capture structured results${C_RESET}"
|
||
echo -e "${C_DIM}Tip: set PERF_API_TOKEN in .env on the server to enable the perf API${C_RESET}\n"
|