test(performance): implement k6 load testing scenarios and java result parser

This commit is contained in:
2026-05-17 02:21:28 +07:00
parent 4a0ae1d615
commit abeb99ed61
19 changed files with 4036 additions and 0 deletions
@@ -0,0 +1,408 @@
/**
* WalkGuide API Client — Reusable HTTP wrapper for k6 tests
* Semua request ke Spring Boot backend melewati modul ini.
*/
import http from "k6/http";
import { check, fail } from "k6";
import { metricsHelper } from "./metrics-helper.js";
const BASE_URL = __ENV.BASE_URL || "http://202.46.28.160:8080";
const API_BASE = `${BASE_URL}/api/v1`;
/**
* Default headers tanpa auth
*/
function jsonHeaders(extra = {}) {
return Object.assign(
{ "Content-Type": "application/json", Accept: "application/json" },
extra,
);
}
/**
* Default headers dengan Bearer token
*/
function authHeaders(token, extra = {}) {
return jsonHeaders(
Object.assign({ Authorization: `Bearer ${token}` }, extra),
);
}
// ──────────────────────────────────────────────
// AUTH endpoints
// ──────────────────────────────────────────────
export function register(payload) {
const res = http.post(`${API_BASE}/auth/register`, JSON.stringify(payload), {
headers: jsonHeaders(),
tags: { endpoint: "register" },
});
metricsHelper.recordEndpoint("register", res);
return res;
}
export function login(email, password) {
const res = http.post(
`${API_BASE}/auth/login`,
JSON.stringify({ email, password }),
{ headers: jsonHeaders(), tags: { endpoint: "login" } },
);
metricsHelper.recordEndpoint("login", res);
return res;
}
export function refreshToken(refreshTok) {
const res = http.post(
`${API_BASE}/auth/refresh`,
JSON.stringify({ refreshToken: refreshTok }),
{ headers: jsonHeaders(), tags: { endpoint: "refresh_token" } },
);
metricsHelper.recordEndpoint("refresh_token", res);
return res;
}
export function logout(token) {
const res = http.post(`${API_BASE}/auth/logout`, null, {
headers: authHeaders(token),
tags: { endpoint: "logout" },
});
metricsHelper.recordEndpoint("logout", res);
return res;
}
export function ping() {
const res = http.get(`${API_BASE}/auth/ping`, { tags: { endpoint: "ping" } });
metricsHelper.recordEndpoint("ping", res);
return res;
}
export function updateFcmToken(token, fcmToken) {
const res = http.post(
`${API_BASE}/auth/fcm-token`,
JSON.stringify({ fcmToken }),
{ headers: authHeaders(token), tags: { endpoint: "fcm_token" } },
);
metricsHelper.recordEndpoint("fcm_token", res);
return res;
}
// ──────────────────────────────────────────────
// PAIRING endpoints
// ──────────────────────────────────────────────
export function inviteUser(token, uniqueUserId) {
const res = http.post(
`${API_BASE}/pairing/invite`,
JSON.stringify({ uniqueUserId }),
{ headers: authHeaders(token), tags: { endpoint: "pairing_invite" } },
);
metricsHelper.recordEndpoint("pairing_invite", res);
return res;
}
export function respondPairing(token, pairingId, accept) {
const res = http.post(
`${API_BASE}/pairing/respond`,
JSON.stringify({ pairingId, accept }),
{ headers: authHeaders(token), tags: { endpoint: "pairing_respond" } },
);
metricsHelper.recordEndpoint("pairing_respond", res);
return res;
}
export function getPairingStatus(token) {
const res = http.get(`${API_BASE}/pairing/status`, {
headers: authHeaders(token),
tags: { endpoint: "pairing_status" },
});
metricsHelper.recordEndpoint("pairing_status", res);
return res;
}
export function unpair(token) {
const res = http.del(`${API_BASE}/pairing/unpair`, null, {
headers: authHeaders(token),
tags: { endpoint: "pairing_unpair" },
});
metricsHelper.recordEndpoint("pairing_unpair", res);
return res;
}
// ──────────────────────────────────────────────
// USER endpoints
// ──────────────────────────────────────────────
export function getUserProfile(token) {
const res = http.get(`${API_BASE}/user/profile`, {
headers: authHeaders(token),
tags: { endpoint: "user_profile" },
});
metricsHelper.recordEndpoint("user_profile", res);
return res;
}
export function updateLocation(
token,
lat,
lng,
accuracy = 5.0,
speed = 1.4,
heading = 90.0,
) {
const res = http.post(
`${API_BASE}/user/location`,
JSON.stringify({ lat, lng, accuracy, speed, heading }),
{ headers: authHeaders(token), tags: { endpoint: "location_update" } },
);
metricsHelper.recordEndpoint("location_update", res);
return res;
}
export function logObstacle(token, payload) {
const res = http.post(`${API_BASE}/user/obstacle`, JSON.stringify(payload), {
headers: authHeaders(token),
tags: { endpoint: "obstacle_log" },
});
metricsHelper.recordEndpoint("obstacle_log", res);
return res;
}
export function triggerSos(token, triggerType, lat, lng) {
const res = http.post(
`${API_BASE}/user/sos`,
JSON.stringify({ triggerType, lat, lng }),
{ headers: authHeaders(token), tags: { endpoint: "sos_trigger" } },
);
metricsHelper.recordEndpoint("sos_trigger", res);
return res;
}
export function getActivityLogs(token, page = 0, size = 20) {
const res = http.get(
`${API_BASE}/user/activity-logs?page=${page}&size=${size}`,
{ headers: authHeaders(token), tags: { endpoint: "activity_logs" } },
);
metricsHelper.recordEndpoint("activity_logs", res);
return res;
}
export function getNotifications(token, page = 0, size = 20) {
const res = http.get(
`${API_BASE}/user/notifications?page=${page}&size=${size}`,
{ headers: authHeaders(token), tags: { endpoint: "notifications" } },
);
metricsHelper.recordEndpoint("notifications", res);
return res;
}
export function getUnreadCount(token) {
const res = http.get(`${API_BASE}/user/notifications/unread-count`, {
headers: authHeaders(token),
tags: { endpoint: "unread_count" },
});
metricsHelper.recordEndpoint("unread_count", res);
return res;
}
export function markAllRead(token) {
const res = http.put(`${API_BASE}/user/notifications/mark-all-read`, null, {
headers: authHeaders(token),
tags: { endpoint: "mark_all_read" },
});
metricsHelper.recordEndpoint("mark_all_read", res);
return res;
}
export function markOneRead(token, notifId) {
const res = http.put(`${API_BASE}/user/notifications/${notifId}/read`, null, {
headers: authHeaders(token),
tags: { endpoint: "mark_one_read" },
});
metricsHelper.recordEndpoint("mark_one_read", res);
return res;
}
export function startWalkguide(token) {
const res = http.post(`${API_BASE}/user/walkguide/start`, null, {
headers: authHeaders(token),
tags: { endpoint: "walkguide_start" },
});
metricsHelper.recordEndpoint("walkguide_start", res);
return res;
}
export function stopWalkguide(token) {
const res = http.post(`${API_BASE}/user/walkguide/stop`, null, {
headers: authHeaders(token),
tags: { endpoint: "walkguide_stop" },
});
metricsHelper.recordEndpoint("walkguide_stop", res);
return res;
}
export function getUserSettings(token) {
const res = http.get(`${API_BASE}/user/settings`, {
headers: authHeaders(token),
tags: { endpoint: "user_settings_get" },
});
metricsHelper.recordEndpoint("user_settings_get", res);
return res;
}
export function updateUserSettings(token, payload) {
const res = http.put(`${API_BASE}/user/settings`, JSON.stringify(payload), {
headers: authHeaders(token),
tags: { endpoint: "user_settings_put" },
});
metricsHelper.recordEndpoint("user_settings_put", res);
return res;
}
export function getUserAiConfig(token) {
const res = http.get(`${API_BASE}/user/ai-config`, {
headers: authHeaders(token),
tags: { endpoint: "ai_config_get" },
});
metricsHelper.recordEndpoint("ai_config_get", res);
return res;
}
// ──────────────────────────────────────────────
// GUARDIAN endpoints
// ──────────────────────────────────────────────
export function getGuardianDashboard(token) {
const res = http.get(`${API_BASE}/guardian/dashboard`, {
headers: authHeaders(token),
tags: { endpoint: "guardian_dashboard" },
});
metricsHelper.recordEndpoint("guardian_dashboard", res);
return res;
}
export function getUserLocation(token) {
const res = http.get(`${API_BASE}/guardian/user-location`, {
headers: authHeaders(token),
tags: { endpoint: "guardian_user_location" },
});
metricsHelper.recordEndpoint("guardian_user_location", res);
return res;
}
export function getLocationHistory(token, page = 0, size = 50) {
const res = http.get(
`${API_BASE}/guardian/location-history?page=${page}&size=${size}`,
{ headers: authHeaders(token), tags: { endpoint: "location_history" } },
);
metricsHelper.recordEndpoint("location_history", res);
return res;
}
export function getObstacleLogs(token, page = 0, size = 20) {
const res = http.get(
`${API_BASE}/guardian/obstacle-logs?page=${page}&size=${size}`,
{ headers: authHeaders(token), tags: { endpoint: "obstacle_logs_get" } },
);
metricsHelper.recordEndpoint("obstacle_logs_get", res);
return res;
}
export function sendNotification(token, payload) {
const res = http.post(
`${API_BASE}/guardian/notifications/send`,
JSON.stringify(payload),
{ headers: authHeaders(token), tags: { endpoint: "send_notification" } },
);
metricsHelper.recordEndpoint("send_notification", res);
return res;
}
export function getSosEvents(token, page = 0, size = 20) {
const res = http.get(
`${API_BASE}/guardian/sos-events?page=${page}&size=${size}`,
{ headers: authHeaders(token), tags: { endpoint: "sos_events_get" } },
);
metricsHelper.recordEndpoint("sos_events_get", res);
return res;
}
export function acknowledgeSos(token, sosId) {
const res = http.put(`${API_BASE}/guardian/sos/${sosId}/acknowledge`, null, {
headers: authHeaders(token),
tags: { endpoint: "sos_acknowledge" },
});
metricsHelper.recordEndpoint("sos_acknowledge", res);
return res;
}
export function getAiConfig(token) {
const res = http.get(`${API_BASE}/guardian/ai-config`, {
headers: authHeaders(token),
tags: { endpoint: "ai_config_guardian_get" },
});
metricsHelper.recordEndpoint("ai_config_guardian_get", res);
return res;
}
export function updateAiConfig(token, payload) {
const res = http.put(
`${API_BASE}/guardian/ai-config`,
JSON.stringify(payload),
{
headers: authHeaders(token),
tags: { endpoint: "ai_config_guardian_put" },
},
);
metricsHelper.recordEndpoint("ai_config_guardian_put", res);
return res;
}
export function getActivityLogsGuardian(token, page = 0, size = 20) {
const res = http.get(
`${API_BASE}/guardian/activity-logs?page=${page}&size=${size}`,
{
headers: authHeaders(token),
tags: { endpoint: "activity_logs_guardian" },
},
);
metricsHelper.recordEndpoint("activity_logs_guardian", res);
return res;
}
export function getGeofenceConfig(token) {
const res = http.get(`${API_BASE}/guardian/geofence`, {
headers: authHeaders(token),
tags: { endpoint: "geofence_get" },
});
metricsHelper.recordEndpoint("geofence_get", res);
return res;
}
export function updateGeofenceConfig(token, payload) {
const res = http.put(
`${API_BASE}/guardian/geofence`,
JSON.stringify(payload),
{ headers: authHeaders(token), tags: { endpoint: "geofence_put" } },
);
metricsHelper.recordEndpoint("geofence_put", res);
return res;
}
// ──────────────────────────────────────────────
// SHARED check helpers
// ──────────────────────────────────────────────
export function checkSuccess(res, label) {
check(res, {
[`${label}: status 2xx`]: (r) => r.status >= 200 && r.status < 300,
[`${label}: has body`]: (r) => r.body && r.body.length > 0,
});
}
export function parseBody(res) {
try {
return JSON.parse(res.body);
} catch (_) {
return null;
}
}
@@ -0,0 +1,168 @@
/**
* WalkGuide — JWT Token Management Helper
* Mengelola login, penyimpanan token, dan auto-refresh untuk k6 VUs.
*/
import { check, sleep } from "k6";
import * as api from "./api-client.js";
import { testData } from "./test-data.js";
// ── Shared token cache (per VU, tidak cross-VU karena k6 isolasi per VU) ────
let _cachedTokens = null; // { accessToken, refreshToken, role, userId, uniqueUserId }
/**
* Login dan simpan token di memory VU ini.
* Return { accessToken, refreshToken, role, userId }
*/
export function loginAndStore(email, password) {
const res = api.login(email, password);
check(res, { "auth-helper: login 200": (r) => r.status === 200 });
if (res.status !== 200) {
console.error(
`[auth-helper] Login failed for ${email}: ${res.status} ${res.body}`,
);
return null;
}
const body = api.parseBody(res);
if (!body || !body.data || !body.data.accessToken) {
console.error(`[auth-helper] Unexpected login response: ${res.body}`);
return null;
}
_cachedTokens = {
accessToken: body.data.accessToken,
refreshToken: body.data.refreshToken,
role: body.data.role,
userId: body.data.userId,
uniqueUserId: body.data.uniqueUserId || null,
};
return _cachedTokens;
}
/**
* Register user baru, lalu login. Return token set.
*/
export function registerAndLogin(payload) {
const regRes = api.register(payload);
check(regRes, {
"auth-helper: register 2xx": (r) => r.status >= 200 && r.status < 300,
});
if (regRes.status !== 200 && regRes.status !== 201) {
// Mungkin email sudah ada — coba login saja
console.warn(
`[auth-helper] Register failed (${regRes.status}), attempting login.`,
);
}
const body = api.parseBody(regRes);
// Kalau register langsung return token (sesuai AuthService):
if (body && body.data && body.data.accessToken) {
_cachedTokens = {
accessToken: body.data.accessToken,
refreshToken: body.data.refreshToken,
role: body.data.role,
userId: body.data.userId,
uniqueUserId: body.data.uniqueUserId || null,
};
return _cachedTokens;
}
// Fallback: login manual
return loginAndStore(payload.email, payload.password);
}
/**
* Ambil access token dari cache. Kalau null, throw agar test gagal eksplisit.
*/
export function getAccessToken() {
if (!_cachedTokens || !_cachedTokens.accessToken) {
throw new Error(
"[auth-helper] No access token in cache. Call loginAndStore() first.",
);
}
return _cachedTokens.accessToken;
}
export function getRefreshToken() {
if (!_cachedTokens || !_cachedTokens.refreshToken) {
throw new Error("[auth-helper] No refresh token in cache.");
}
return _cachedTokens.refreshToken;
}
export function getCachedTokens() {
return _cachedTokens;
}
export function getCachedUniqueUserId() {
return _cachedTokens ? _cachedTokens.uniqueUserId : null;
}
/**
* Refresh access token menggunakan refresh token yang tersimpan.
* Update cache dengan access token baru.
*/
export function doRefreshToken() {
const rt = getRefreshToken();
const res = api.refreshToken(rt);
check(res, { "auth-helper: refresh 200": (r) => r.status === 200 });
if (res.status !== 200) {
console.error(`[auth-helper] Refresh token failed: ${res.status}`);
return null;
}
const body = api.parseBody(res);
if (body && body.data && body.data.accessToken) {
_cachedTokens.accessToken = body.data.accessToken;
}
return _cachedTokens.accessToken;
}
/**
* Clear semua cached token (simulasi logout).
*/
export function clearTokens() {
_cachedTokens = null;
}
// ── Pre-built credential sets untuk smoke/load test ──────────────────────────
// Test users yang sudah ada di DB kampus (V2__seed_users.sql)
// Jika tidak ada, gunakan dynamic register.
export const PREBUILT_GUARDIAN = {
email: __ENV.GUARDIAN_EMAIL || "guardian@walkguide.test",
password: __ENV.GUARDIAN_PASSWORD || "Guardian123!",
};
export const PREBUILT_USER = {
email: __ENV.USER_EMAIL || "user@walkguide.test",
password: __ENV.USER_PASSWORD || "User123!",
};
/**
* Setup pair: register guardian + user, lalu pairing.
* Return { guardianTokens, userTokens }
*/
export function setupPairedSession() {
const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
// Register guardian
const gPayload = testData.guardianRegisterPayload(suffix);
const gTokens = registerAndLogin(gPayload);
if (!gTokens) return null;
// Register user
const uPayload = testData.userRegisterPayload(suffix);
const uTokens = registerAndLogin(uPayload);
if (!uTokens) return null;
return {
guardianTokens: gTokens,
userTokens: uTokens,
guardianPayload: gPayload,
userPayload: uPayload,
};
}
@@ -0,0 +1,259 @@
/**
* WalkGuide — Custom Metric Collectors
* Menambah metric k6 custom di atas default http_req_* metrics.
*
* METRIC YANG DIKUMPULKAN (sesuai ketentuan pillar 3):
* 1. throughput — req/s (via http_reqs counter bawaan k6 + custom trend)
* 2. p95 latency — custom trend per endpoint
* 3. error rate — custom rate
* 4. db query time — diperkirakan via response time karena tidak ada akses langsung DB
* 5. jvm heap — via Spring Actuator /actuator/metrics/jvm.memory.used
*/
import { Counter, Gauge, Rate, Trend } from "k6/metrics";
import http from "k6/http";
// ── Custom Metrics ────────────────────────────────────────────────────────────
/** Total request error rate (non-2xx) */
export const errorRate = new Rate("walkguide_error_rate");
/** Request latency per endpoint */
export const endpointLatency = new Trend("walkguide_endpoint_latency_ms", true);
/** Location update latency khusus (endpoint paling kritis) */
export const locationLatency = new Trend("walkguide_location_latency_ms", true);
/** Obstacle log latency khusus */
export const obstacleLatency = new Trend("walkguide_obstacle_latency_ms", true);
/** SOS trigger latency khusus (harus paling cepat) */
export const sosLatency = new Trend("walkguide_sos_latency_ms", true);
/** Notification send latency */
export const notifLatency = new Trend("walkguide_notif_latency_ms", true);
/** Auth latency */
export const authLatency = new Trend("walkguide_auth_latency_ms", true);
/** Timeline query latency */
export const timelineLatency = new Trend("walkguide_timeline_latency_ms", true);
/** Pairing flow latency */
export const pairingLatency = new Trend("walkguide_pairing_latency_ms", true);
/** Successful requests counter */
export const successfulRequests = new Counter("walkguide_successful_requests");
/** Failed requests counter */
export const failedRequests = new Counter("walkguide_failed_requests");
/** JVM heap usage (MB) — populated via actuator polling */
export const jvmHeapUsed = new Gauge("walkguide_jvm_heap_used_mb");
/** Active DB connections estimate (via actuator) */
export const dbConnections = new Gauge("walkguide_db_connections");
// ── Endpoint → metric mapping ─────────────────────────────────────────────────
const ENDPOINT_METRIC_MAP = {
location_update: locationLatency,
obstacle_log: obstacleLatency,
sos_trigger: sosLatency,
send_notification: notifLatency,
login: authLatency,
register: authLatency,
refresh_token: authLatency,
location_history: timelineLatency,
activity_logs: timelineLatency,
activity_logs_guardian: timelineLatency,
pairing_invite: pairingLatency,
pairing_respond: pairingLatency,
};
// ── metricsHelper object ──────────────────────────────────────────────────────
export const metricsHelper = {
/**
* Catat metric untuk setiap response.
* @param {string} endpointTag — string yang di-pass sebagai tags.endpoint
* @param {Response} res — k6 http response object
*/
recordEndpoint(endpointTag, res) {
const isSuccess = res.status >= 200 && res.status < 300;
const durationMs = res.timings.duration;
// Error rate
errorRate.add(!isSuccess);
// Generic endpoint latency
endpointLatency.add(durationMs, { endpoint: endpointTag });
// Specific metric kalau ada di map
if (ENDPOINT_METRIC_MAP[endpointTag]) {
ENDPOINT_METRIC_MAP[endpointTag].add(durationMs);
}
// Success / fail counter
if (isSuccess) {
successfulRequests.add(1);
} else {
failedRequests.add(1);
}
},
/**
* Poll Spring Actuator untuk JVM heap.
* Call ini dari setup() atau secara periodik dari satu VU khusus.
* @param {string} baseUrl
* @param {string} token — optional, actuator mungkin butuh auth
*/
pollJvmHeap(baseUrl, token = null) {
const headers = token
? { Authorization: `Bearer ${token}`, Accept: "application/json" }
: { Accept: "application/json" };
try {
const res = http.get(`${baseUrl}/actuator/metrics/jvm.memory.used`, {
headers,
timeout: "5s",
tags: { endpoint: "actuator_jvm" },
});
if (res.status === 200) {
const body = JSON.parse(res.body);
// measurement[0].value adalah bytes
const heapMb = body.measurements[0].value / (1024 * 1024);
jvmHeapUsed.add(heapMb);
}
} catch (e) {
// Actuator mungkin tidak public — skip
}
},
/**
* Poll Spring Actuator untuk DB connection pool (HikariCP).
* @param {string} baseUrl
* @param {string} token
*/
pollDbConnections(baseUrl, token = null) {
const headers = token
? { Authorization: `Bearer ${token}`, Accept: "application/json" }
: { Accept: "application/json" };
try {
const res = http.get(
`${baseUrl}/actuator/metrics/hikaricp.connections.active`,
{ headers, timeout: "5s", tags: { endpoint: "actuator_db" } },
);
if (res.status === 200) {
const body = JSON.parse(res.body);
const active = body.measurements[0].value;
dbConnections.add(active);
}
} catch (e) {
// Skip
}
},
/**
* Generate ringkasan teks dari custom metrics (untuk print di teardown).
*/
summary() {
return {
errorRate: errorRate,
locationP95: locationLatency,
obstacleP95: obstacleLatency,
sosP95: sosLatency,
notifP95: notifLatency,
authP95: authLatency,
timelineP95: timelineLatency,
pairingP95: pairingLatency,
};
},
};
/**
* Default thresholds yang bisa di-reuse di semua scenario options.
* Sesuai ketentuan pillar 3: p95 < 500ms, error rate < 1%.
*/
export const DEFAULT_THRESHOLDS = {
// ── HTTP default k6
http_req_duration: ["p(95)<500", "p(99)<1000"],
http_req_failed: ["rate<0.01"],
// ── WalkGuide custom
walkguide_error_rate: ["rate<0.01"],
walkguide_endpoint_latency_ms: ["p(95)<500"],
walkguide_location_latency_ms: ["p(95)<300"], // location harus cepat
walkguide_obstacle_latency_ms: ["p(95)<400"],
walkguide_sos_latency_ms: ["p(95)<200"], // SOS PALING KRITIS
walkguide_notif_latency_ms: ["p(95)<500"],
walkguide_auth_latency_ms: ["p(95)<800"],
walkguide_timeline_latency_ms: ["p(95)<1000"], // query analytics boleh lebih lambat
walkguide_pairing_latency_ms: ["p(95)<600"],
};
/**
* Relaxed thresholds untuk stress test (ekspektasi lebih longgar).
*/
export const STRESS_THRESHOLDS = {
http_req_duration: ["p(95)<1500", "p(99)<3000"],
http_req_failed: ["rate<0.05"],
walkguide_error_rate: ["rate<0.05"],
walkguide_sos_latency_ms: ["p(95)<500"],
};
/**
* handleSummary helper — generate output JSON + text.
* Import di tiap scenario: export { handleSummary } from '../modules/metrics-helper.js'
*/
export function handleSummary(data) {
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const fileName = `k6-results/summary_${stamp}.json`;
return {
stdout: textSummary(data),
[fileName]: JSON.stringify(data, null, 2),
};
}
function textSummary(data) {
const { metrics } = data;
const lines = [
"═══════════════════════════════════════════════════════════════",
" WalkGuide Load Test — Result Summary",
"═══════════════════════════════════════════════════════════════",
];
const pick = (name, stat) => {
if (metrics[name]) {
const val = metrics[name].values[stat];
return val !== undefined ? val.toFixed(2) : "N/A";
}
return "N/A";
};
lines.push(
` http_req_duration p95=${pick("http_req_duration", "p(95)")}ms p99=${pick("http_req_duration", "p(99)")}ms`,
);
lines.push(` http_req_failed rate=${pick("http_req_failed", "rate")}`);
lines.push(
` sos_latency p95=${pick("walkguide_sos_latency_ms", "p(95)")}ms`,
);
lines.push(
` location_latency p95=${pick("walkguide_location_latency_ms", "p(95)")}ms`,
);
lines.push(
` obstacle_latency p95=${pick("walkguide_obstacle_latency_ms", "p(95)")}ms`,
);
lines.push(
` notif_latency p95=${pick("walkguide_notif_latency_ms", "p(95)")}ms`,
);
lines.push(
` auth_latency p95=${pick("walkguide_auth_latency_ms", "p(95)")}ms`,
);
lines.push(
` timeline_latency p95=${pick("walkguide_timeline_latency_ms", "p(95)")}ms`,
);
lines.push("═══════════════════════════════════════════════════════════════");
return lines.join("\n") + "\n";
}
@@ -0,0 +1,265 @@
/**
* WalkGuide — Synthetic Test Data Generator
* Menghasilkan data dummy yang realistis untuk semua skenario load test.
*/
import { uuidv4 } from "https://jslib.k6.io/k6-utils/1.4.0/index.js";
// ── Alphabet untuk uniqueUserId (12 char alphanumeric) ──────────────────────
const ALPHANUM =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
function randomString(len, chars = ALPHANUM) {
let out = "";
for (let i = 0; i < len; i++) {
out += chars[Math.floor(Math.random() * chars.length)];
}
return out;
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomFloat(min, max, decimals = 6) {
return parseFloat((Math.random() * (max - min) + min).toFixed(decimals));
}
function randomElement(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// ── Koordinat realistis: sekitar Surabaya ───────────────────────────────────
const SURABAYA_CENTER = { lat: -7.2575, lng: 112.7521 };
export function randomSurabayaCoord(radiusDegrees = 0.05) {
return {
lat: SURABAYA_CENTER.lat + randomFloat(-radiusDegrees, radiusDegrees),
lng: SURABAYA_CENTER.lng + randomFloat(-radiusDegrees, radiusDegrees),
};
}
// ── User / Guardian data ─────────────────────────────────────────────────────
/**
* Generate payload untuk register GUARDIAN
*/
export function guardianRegisterPayload(suffix = "") {
const uid = suffix || randomString(8);
return {
email: `guardian_${uid}@walkguide-test.com`,
password: `Pass@${randomString(6)}!`,
displayName: `Guardian ${uid}`,
role: "ROLE_GUARDIAN",
};
}
/**
* Generate payload untuk register USER (tunanetra)
*/
export function userRegisterPayload(suffix = "") {
const uid = suffix || randomString(8);
return {
email: `user_${uid}@walkguide-test.com`,
password: `Pass@${randomString(6)}!`,
displayName: `User ${uid}`,
role: "ROLE_USER",
};
}
// ── Location data ────────────────────────────────────────────────────────────
const DIRECTION_LABELS = [
"NORTH",
"NORTHEAST",
"EAST",
"SOUTHEAST",
"SOUTH",
"SOUTHWEST",
"WEST",
"NORTHWEST",
];
/**
* Generate satu location update payload (simulasi GPS setiap 5 detik)
*/
export function locationUpdatePayload(
baseLat = SURABAYA_CENTER.lat,
baseLng = SURABAYA_CENTER.lng,
) {
return {
lat: baseLat + randomFloat(-0.001, 0.001),
lng: baseLng + randomFloat(-0.001, 0.001),
accuracy: randomFloat(3.0, 15.0, 1),
speed: randomFloat(0.5, 2.5, 2), // walking speed m/s
heading: randomFloat(0.0, 359.9, 1),
};
}
/**
* Generate array of location updates (simulasi walking path)
*/
export function walkingPath(
steps = 10,
startLat = SURABAYA_CENTER.lat,
startLng = SURABAYA_CENTER.lng,
) {
const path = [];
let lat = startLat,
lng = startLng;
for (let i = 0; i < steps; i++) {
lat += randomFloat(-0.0002, 0.0002);
lng += randomFloat(-0.0002, 0.0002);
path.push({
lat,
lng,
accuracy: randomFloat(3, 10, 1),
speed: randomFloat(0.8, 1.8, 2),
heading: randomFloat(0, 360, 1),
});
}
return path;
}
// ── Obstacle data ────────────────────────────────────────────────────────────
const YOLO_LABELS = [
"person",
"car",
"motorcycle",
"bicycle",
"truck",
"bus",
"traffic light",
"stop sign",
"bench",
"chair",
"potted plant",
"fire hydrant",
"parking meter",
"dog",
"cat",
"suitcase",
];
const OBSTACLE_DIRECTIONS = ["LEFT", "CENTER", "RIGHT"];
const OBSTACLE_DISTANCES = ["Very Close", "Close", "Medium", "Far"];
/**
* Generate satu obstacle log payload (output YOLO detection)
*/
export function obstacleLogPayload() {
const coord = randomSurabayaCoord(0.02);
return {
label: randomElement(YOLO_LABELS),
confidence: randomFloat(0.5, 0.99, 4),
direction: randomElement(OBSTACLE_DIRECTIONS),
estimatedDist: randomElement(OBSTACLE_DISTANCES),
lat: coord.lat,
lng: coord.lng,
};
}
/**
* Generate burst obstacle logs (simulasi: 5 FPS deteksi selama beberapa detik)
*/
export function obstacleLogBurst(count = 5) {
return Array.from({ length: count }, () => obstacleLogPayload());
}
// ── SOS data ─────────────────────────────────────────────────────────────────
const SOS_TRIGGER_TYPES = ["VOICE_COMMAND", "BUTTON", "MANUAL"];
export function sosPayload() {
const coord = randomSurabayaCoord(0.03);
return {
triggerType: randomElement(SOS_TRIGGER_TYPES),
lat: coord.lat,
lng: coord.lng,
};
}
// ── Notification data ─────────────────────────────────────────────────────────
const NOTIFICATION_MESSAGES = [
"Hati-hati ada kendaraan di depan!",
"Sudah sampai tujuan belum?",
"Minta kabar, kamu baik-baik saja?",
"Ada yang bisa dibantu?",
"Jangan lupa berhati-hati di jalan.",
"Aku pantau dari sini ya.",
"Sudah mau hujan, cari tempat teduh.",
"Ada mobil di sebelah kanan!",
"Tolong berhenti dulu.",
"Aman ya? Jawab kalau dengar.",
];
export function sendNotificationPayload() {
return {
notifType: "TEXT",
content: randomElement(NOTIFICATION_MESSAGES),
};
}
// ── AI Config data ────────────────────────────────────────────────────────────
export function aiConfigPayload() {
return {
confidenceThreshold: randomFloat(0.3, 0.8, 2),
alertDistanceClose: randomFloat(1.0, 2.5, 1),
alertDistanceMedium: randomFloat(2.5, 5.0, 1),
maxInferenceFps: randomInt(3, 10),
enabledLabels: "ALL",
};
}
// ── User Settings data ────────────────────────────────────────────────────────
export function userSettingsPayload() {
return {
ttsLanguage: randomElement(["id-ID", "en-US"]),
ttsPitch: randomFloat(0.8, 1.5, 1),
ttsSpeed: randomFloat(0.7, 1.3, 1),
warnNoGuardian: true,
hapticEnabled: true,
};
}
// ── Geofence Config data ──────────────────────────────────────────────────────
export function geofenceConfigPayload() {
const coord = randomSurabayaCoord(0.01);
return {
centerLat: coord.lat,
centerLng: coord.lng,
radiusMeters: randomFloat(200, 1000, 0),
enabled: true,
};
}
// ── Unique user id (mirror dari backend logic) ───────────────────────────────
export function fakeUniqueUserId() {
return randomString(12);
}
// ── Generic helpers ───────────────────────────────────────────────────────────
export const testData = {
guardianRegisterPayload,
userRegisterPayload,
locationUpdatePayload,
walkingPath,
obstacleLogPayload,
obstacleLogBurst,
sosPayload,
sendNotificationPayload,
aiConfigPayload,
userSettingsPayload,
geofenceConfigPayload,
fakeUniqueUserId,
randomSurabayaCoord,
randomElement,
randomInt,
randomFloat,
};