test(performance): implement k6 load testing scenarios and java result parser
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* WalkGuide — k6 HTML Report Generator (Node.js)
|
||||
* Mengkonversi parsed-report.json menjadi HTML report yang rapi untuk submission.
|
||||
*
|
||||
* Usage:
|
||||
* node html-reporter.js <parsed-report.json> [output.html]
|
||||
*
|
||||
* Output: HTML report siap lampir ke laporan final exam.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const inputFile = process.argv[2];
|
||||
const outputFile = process.argv[3] || "k6-results/load-test-report.html";
|
||||
|
||||
if (!inputFile || !fs.existsSync(inputFile)) {
|
||||
console.error(
|
||||
"Usage: node html-reporter.js <parsed-report.json> [output.html]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const report = JSON.parse(fs.readFileSync(inputFile, "utf8"));
|
||||
|
||||
// ── HTML Template ─────────────────────────────────────────────────────────────
|
||||
function badge(result) {
|
||||
if (!result) return '<span class="badge skip">SKIP</span>';
|
||||
if (result.includes("PASS")) return '<span class="badge pass">✅ PASS</span>';
|
||||
if (result.includes("FAIL")) return '<span class="badge fail">❌ FAIL</span>';
|
||||
return `<span class="badge skip">${result}</span>`;
|
||||
}
|
||||
|
||||
function metricRow(label, val) {
|
||||
return `<tr><td>${label}</td><td><strong>${val || "N/A"}</strong></td></tr>`;
|
||||
}
|
||||
|
||||
function endpointTableRows() {
|
||||
const rows = [];
|
||||
for (const [ep, metrics] of Object.entries(report.endpoints || {})) {
|
||||
const dur = metrics["http_req_duration"];
|
||||
if (!dur) continue;
|
||||
rows.push(`
|
||||
<tr>
|
||||
<td><code>${ep}</code></td>
|
||||
<td>${dur.count || "—"}</td>
|
||||
<td>${dur.avg || "—"}</td>
|
||||
<td>${dur.p95 || "—"}</td>
|
||||
<td>${dur.p99 || "—"}</td>
|
||||
<td>${dur.max || "—"}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
return rows.join("");
|
||||
}
|
||||
|
||||
function thresholdRows() {
|
||||
return (report.thresholdResults || [])
|
||||
.map(
|
||||
(t) => `
|
||||
<tr>
|
||||
<td>${t.name}</td>
|
||||
<td><code>${t.metric}</code></td>
|
||||
<td>${t.threshold}</td>
|
||||
<td>${t.actual}</td>
|
||||
<td>${badge(t.result)}</td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function wgMetricCards() {
|
||||
const wg = report.walkguideMetrics || {};
|
||||
const cards = [
|
||||
{ label: "🔐 Auth", key: "authLatency", threshold: "< 800ms" },
|
||||
{ label: "📍 Location", key: "locationLatency", threshold: "< 300ms" },
|
||||
{ label: "🚧 Obstacle", key: "obstacleLatency", threshold: "< 400ms" },
|
||||
{ label: "🚨 SOS", key: "sosLatency", threshold: "< 200ms" },
|
||||
{ label: "📬 Notif", key: "notifLatency", threshold: "< 500ms" },
|
||||
{ label: "📊 Timeline", key: "timelineLatency", threshold: "< 1000ms" },
|
||||
{ label: "🔗 Pairing", key: "pairingLatency", threshold: "< 600ms" },
|
||||
];
|
||||
|
||||
return cards
|
||||
.map((c) => {
|
||||
const data = wg[c.key];
|
||||
return `
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">${c.label}</div>
|
||||
<div class="metric-p95">${data ? data.p95 : "N/A"}</div>
|
||||
<div class="metric-sub">p95 | threshold ${c.threshold}</div>
|
||||
<div class="metric-avg">avg: ${data ? data.avg : "N/A"} | p99: ${data ? data.p99 : "N/A"}</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>WalkGuide — k6 Load Test Report</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
|
||||
h1 { font-size: 2rem; color: #38bdf8; margin-bottom: 4px; }
|
||||
h2 { font-size: 1.2rem; color: #7dd3fc; margin: 32px 0 12px; border-bottom: 1px solid #1e3a5f; padding-bottom: 6px; }
|
||||
p { color: #94a3b8; margin-bottom: 8px; }
|
||||
.header { margin-bottom: 32px; }
|
||||
.header .meta { color: #64748b; font-size: 0.85rem; margin-top: 4px; }
|
||||
|
||||
/* Key Metrics Grid */
|
||||
.key-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||
.key-card { background: #1e293b; border-radius: 10px; padding: 18px; border: 1px solid #334155; }
|
||||
.key-card .klabel { font-size: 0.75rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.key-card .kvalue { font-size: 1.5rem; font-weight: 700; color: #f0f9ff; margin: 6px 0 2px; }
|
||||
.key-card .kdesc { font-size: 0.72rem; color: #475569; }
|
||||
|
||||
/* WalkGuide Metric Cards */
|
||||
.wg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
|
||||
.metric-card { background: #1e293b; border-radius: 8px; padding: 14px; border: 1px solid #1e3a5f; text-align: center; }
|
||||
.metric-label { font-size: 0.85rem; color: #94a3b8; margin-bottom: 6px; }
|
||||
.metric-p95 { font-size: 1.6rem; font-weight: 700; color: #38bdf8; }
|
||||
.metric-sub { font-size: 0.7rem; color: #475569; margin-top: 2px; }
|
||||
.metric-avg { font-size: 0.72rem; color: #64748b; margin-top: 4px; }
|
||||
|
||||
/* Tables */
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 24px; font-size: 0.88rem; }
|
||||
th { background: #1e3a5f; color: #7dd3fc; text-align: left; padding: 10px 12px; }
|
||||
td { padding: 9px 12px; border-bottom: 1px solid #1e293b; color: #cbd5e1; }
|
||||
tr:nth-child(even) td { background: #0f1f35; }
|
||||
code { background: #1e293b; padding: 2px 6px; border-radius: 4px; font-size: 0.82rem; color: #38bdf8; }
|
||||
|
||||
/* Badges */
|
||||
.badge { padding: 3px 10px; border-radius: 12px; font-size: 0.78rem; font-weight: 600; }
|
||||
.badge.pass { background: #064e3b; color: #34d399; }
|
||||
.badge.fail { background: #450a0a; color: #f87171; }
|
||||
.badge.skip { background: #1e293b; color: #94a3b8; }
|
||||
|
||||
footer { margin-top: 40px; text-align: center; color: #334155; font-size: 0.78rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🦺 WalkGuide — k6 Load Test Report</h1>
|
||||
<p>Spring Boot Backend Performance Benchmark — Pillar 3</p>
|
||||
<div class="meta">
|
||||
Generated: ${report.generatedAt} |
|
||||
Input: ${path.basename(report.inputFile || "")} |
|
||||
Total Data Points: ${report.totalPoints}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>📊 Key Metrics (Pillar 3 — 5 Required)</h2>
|
||||
<div class="key-grid">
|
||||
<div class="key-card">
|
||||
<div class="klabel">1. Throughput</div>
|
||||
<div class="kvalue">${report.keyMetrics.throughput.value || "N/A"}</div>
|
||||
<div class="kdesc">Requests per second</div>
|
||||
</div>
|
||||
<div class="key-card">
|
||||
<div class="klabel">2. p95 Latency</div>
|
||||
<div class="kvalue">${report.keyMetrics.p95Latency.value || "N/A"}</div>
|
||||
<div class="kdesc">95th percentile response time</div>
|
||||
</div>
|
||||
<div class="key-card">
|
||||
<div class="klabel">3. Error Rate</div>
|
||||
<div class="kvalue">${report.keyMetrics.errorRate.value || "N/A"}</div>
|
||||
<div class="kdesc">Non-2xx responses | ${report.keyMetrics.errorRate.passFail || "—"}</div>
|
||||
</div>
|
||||
<div class="key-card">
|
||||
<div class="klabel">4. DB Query Time</div>
|
||||
<div class="kvalue">${report.keyMetrics.dbQueryTime.value || "N/A"}</div>
|
||||
<div class="kdesc">Estimated via write endpoint p95</div>
|
||||
</div>
|
||||
<div class="key-card">
|
||||
<div class="klabel">5. JVM Heap</div>
|
||||
<div class="kvalue">${report.keyMetrics.jvmHeap.value || "N/A"}</div>
|
||||
<div class="kdesc">Spring Actuator jvm.memory.used</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>🏃 WalkGuide Endpoint Latency Breakdown</h2>
|
||||
<div class="wg-grid">${wgMetricCards()}</div>
|
||||
|
||||
<h2>✅ Threshold Evaluation</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Check</th><th>Metric</th><th>Threshold</th><th>Actual</th><th>Result</th></tr>
|
||||
</thead>
|
||||
<tbody>${thresholdRows()}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>📋 Per-Endpoint Latency (ms)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Endpoint</th><th>Requests</th><th>Avg</th><th>p95</th><th>p99</th><th>Max</th></tr>
|
||||
</thead>
|
||||
<tbody>${endpointTableRows()}</tbody>
|
||||
</table>
|
||||
|
||||
<footer>
|
||||
WalkGuide AI — Integrated Mobile Application Project Final Exam |
|
||||
Generated by html-reporter.js
|
||||
</footer>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// ── Write ─────────────────────────────────────────────────────────────────────
|
||||
const outDir = path.dirname(outputFile);
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(outputFile, html);
|
||||
console.log(`✅ HTML report written to: ${outputFile}`);
|
||||
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* WalkGuide — k6 JSON Result Parser (Node.js)
|
||||
* Parse output dari k6 --out json=output.ndjson dan generate readable report.
|
||||
*
|
||||
* Usage:
|
||||
* node result-parser.js <input.ndjson> [output-report.json]
|
||||
*
|
||||
* k6 JSON format: satu JSON object per baris (newline-delimited JSON / NDJSON)
|
||||
* Setiap baris adalah metric data point atau test info.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// ── Parse args ────────────────────────────────────────────────────────────────
|
||||
const inputFile = process.argv[2];
|
||||
const outputFile = process.argv[3] || "k6-results/parsed-report.json";
|
||||
|
||||
if (!inputFile) {
|
||||
console.error(
|
||||
"Usage: node result-parser.js <k6-output.ndjson> [output.json]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`Input file not found: ${inputFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Read and parse NDJSON ─────────────────────────────────────────────────────
|
||||
const raw = fs.readFileSync(inputFile, "utf8");
|
||||
const lines = raw.trim().split("\n").filter(Boolean);
|
||||
|
||||
const dataPoints = [];
|
||||
const errors = [];
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
dataPoints.push(JSON.parse(line));
|
||||
} catch (e) {
|
||||
errors.push({ line: line.slice(0, 100), error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Parsed ${dataPoints.length} data points, ${errors.length} parse errors.`,
|
||||
);
|
||||
|
||||
// ── Aggregate metrics ─────────────────────────────────────────────────────────
|
||||
const metricAggregates = {};
|
||||
|
||||
for (const dp of dataPoints) {
|
||||
if (dp.type !== "Point") continue;
|
||||
|
||||
const metricName = dp.metric;
|
||||
const value = dp.data && dp.data.value !== undefined ? dp.data.value : 0;
|
||||
const tags = dp.data && dp.data.tags ? dp.data.tags : {};
|
||||
|
||||
if (!metricAggregates[metricName]) {
|
||||
metricAggregates[metricName] = {
|
||||
values: [],
|
||||
byEndpoint: {},
|
||||
};
|
||||
}
|
||||
|
||||
metricAggregates[metricName].values.push(value);
|
||||
|
||||
// Group by endpoint tag
|
||||
const endpoint = tags.endpoint || "unknown";
|
||||
if (!metricAggregates[metricName].byEndpoint[endpoint]) {
|
||||
metricAggregates[metricName].byEndpoint[endpoint] = [];
|
||||
}
|
||||
metricAggregates[metricName].byEndpoint[endpoint].push(value);
|
||||
}
|
||||
|
||||
// ── Statistical helpers ───────────────────────────────────────────────────────
|
||||
function percentile(arr, p) {
|
||||
if (!arr || arr.length === 0) return 0;
|
||||
const sorted = [...arr].sort((a, b) => a - b);
|
||||
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
||||
return sorted[Math.max(0, idx)];
|
||||
}
|
||||
|
||||
function avg(arr) {
|
||||
if (!arr || arr.length === 0) return 0;
|
||||
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||
}
|
||||
|
||||
function summarizeSeries(arr) {
|
||||
if (!arr || arr.length === 0) return null;
|
||||
return {
|
||||
count: arr.length,
|
||||
min: Math.min(...arr).toFixed(2),
|
||||
max: Math.max(...arr).toFixed(2),
|
||||
avg: avg(arr).toFixed(2),
|
||||
p50: percentile(arr, 50).toFixed(2),
|
||||
p90: percentile(arr, 90).toFixed(2),
|
||||
p95: percentile(arr, 95).toFixed(2),
|
||||
p99: percentile(arr, 99).toFixed(2),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Build structured report ───────────────────────────────────────────────────
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
inputFile: path.resolve(inputFile),
|
||||
totalPoints: dataPoints.length,
|
||||
parseErrors: errors.length,
|
||||
|
||||
// 5 KEY METRICS sesuai ketentuan pillar 3:
|
||||
keyMetrics: {
|
||||
// 1. Throughput — request per second
|
||||
throughput: {
|
||||
label: "Throughput (req/s)",
|
||||
description: "Total HTTP requests divided by test duration",
|
||||
value: null, // dihitung di bawah
|
||||
},
|
||||
// 2. p95 Latency — overall
|
||||
p95Latency: {
|
||||
label: "p95 Response Time (ms)",
|
||||
description: "95th percentile of all HTTP request durations",
|
||||
value: null,
|
||||
},
|
||||
// 3. Error Rate
|
||||
errorRate: {
|
||||
label: "Error Rate (%)",
|
||||
description: "Percentage of non-2xx HTTP responses",
|
||||
value: null,
|
||||
passFail: null,
|
||||
},
|
||||
// 4. DB Query Time (estimated via response time)
|
||||
dbQueryTime: {
|
||||
label: "Estimated DB Query Time (ms)",
|
||||
description: "p95 of write endpoints (location + obstacle) as DB proxy",
|
||||
value: null,
|
||||
note: "Actual DB time requires Spring Actuator or DB metrics",
|
||||
},
|
||||
// 5. JVM Heap
|
||||
jvmHeap: {
|
||||
label: "JVM Heap Used (MB)",
|
||||
description: "From Spring Actuator metric, collected during test",
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
|
||||
// Per-metric detailed stats
|
||||
metrics: {},
|
||||
|
||||
// Per-endpoint breakdown
|
||||
endpoints: {},
|
||||
|
||||
// WalkGuide-specific breakdown
|
||||
walkguideMetrics: {
|
||||
authLatency: null,
|
||||
locationLatency: null,
|
||||
obstacleLatency: null,
|
||||
sosLatency: null,
|
||||
notifLatency: null,
|
||||
timelineLatency: null,
|
||||
pairingLatency: null,
|
||||
},
|
||||
|
||||
// Threshold evaluation
|
||||
thresholdResults: [],
|
||||
};
|
||||
|
||||
// ── Populate metrics ──────────────────────────────────────────────────────────
|
||||
for (const [name, data] of Object.entries(metricAggregates)) {
|
||||
report.metrics[name] = summarizeSeries(data.values);
|
||||
|
||||
// Per-endpoint
|
||||
for (const [ep, vals] of Object.entries(data.byEndpoint)) {
|
||||
if (!report.endpoints[ep]) report.endpoints[ep] = {};
|
||||
report.endpoints[ep][name] = summarizeSeries(vals);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fill key metrics ──────────────────────────────────────────────────────────
|
||||
if (report.metrics["http_req_duration"]) {
|
||||
report.keyMetrics.p95Latency.value =
|
||||
report.metrics["http_req_duration"].p95 + " ms";
|
||||
}
|
||||
|
||||
if (report.metrics["http_req_failed"]) {
|
||||
const rate = (
|
||||
parseFloat(report.metrics["http_req_failed"].avg) * 100
|
||||
).toFixed(2);
|
||||
report.keyMetrics.errorRate.value = rate + "%";
|
||||
report.keyMetrics.errorRate.passFail = parseFloat(rate) < 1 ? "PASS" : "FAIL";
|
||||
}
|
||||
|
||||
if (report.metrics["walkguide_jvm_heap_used_mb"]) {
|
||||
report.keyMetrics.jvmHeap.value =
|
||||
report.metrics["walkguide_jvm_heap_used_mb"].avg + " MB";
|
||||
}
|
||||
|
||||
// DB estimate: avg of location_update + obstacle_log p95
|
||||
const dbMetrics = [
|
||||
"walkguide_location_latency_ms",
|
||||
"walkguide_obstacle_latency_ms",
|
||||
];
|
||||
const dbVals = dbMetrics
|
||||
.filter((m) => report.metrics[m])
|
||||
.map((m) => parseFloat(report.metrics[m].p95));
|
||||
if (dbVals.length > 0) {
|
||||
report.keyMetrics.dbQueryTime.value =
|
||||
(dbVals.reduce((a, b) => a + b, 0) / dbVals.length).toFixed(2) + " ms";
|
||||
}
|
||||
|
||||
// Throughput: dari http_reqs total / test duration (estimasi dari data points)
|
||||
if (report.metrics["http_reqs"]) {
|
||||
const totalReqs = report.metrics["http_reqs"].count;
|
||||
// Estimasi durasi dari timestamps
|
||||
const timestamps = dataPoints
|
||||
.filter((dp) => dp.type === "Point" && dp.metric === "http_reqs")
|
||||
.map((dp) => new Date(dp.data.time).getTime());
|
||||
if (timestamps.length > 1) {
|
||||
const durationS =
|
||||
(Math.max(...timestamps) - Math.min(...timestamps)) / 1000;
|
||||
report.keyMetrics.throughput.value =
|
||||
(totalReqs / durationS).toFixed(1) + " req/s";
|
||||
} else {
|
||||
report.keyMetrics.throughput.value = totalReqs + " total requests";
|
||||
}
|
||||
}
|
||||
|
||||
// ── WalkGuide-specific ────────────────────────────────────────────────────────
|
||||
const wgMetricMap = {
|
||||
authLatency: "walkguide_auth_latency_ms",
|
||||
locationLatency: "walkguide_location_latency_ms",
|
||||
obstacleLatency: "walkguide_obstacle_latency_ms",
|
||||
sosLatency: "walkguide_sos_latency_ms",
|
||||
notifLatency: "walkguide_notif_latency_ms",
|
||||
timelineLatency: "walkguide_timeline_latency_ms",
|
||||
pairingLatency: "walkguide_pairing_latency_ms",
|
||||
};
|
||||
|
||||
for (const [key, metricName] of Object.entries(wgMetricMap)) {
|
||||
if (report.metrics[metricName]) {
|
||||
report.walkguideMetrics[key] = {
|
||||
p95: report.metrics[metricName].p95 + " ms",
|
||||
p99: report.metrics[metricName].p99 + " ms",
|
||||
avg: report.metrics[metricName].avg + " ms",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Threshold checks ──────────────────────────────────────────────────────────
|
||||
const thresholdChecks = [
|
||||
{
|
||||
name: "p95 overall < 500ms",
|
||||
metric: "http_req_duration",
|
||||
stat: "p95",
|
||||
threshold: 500,
|
||||
unit: "ms",
|
||||
operator: "<",
|
||||
},
|
||||
{
|
||||
name: "error rate < 1%",
|
||||
metric: "http_req_failed",
|
||||
stat: "avg",
|
||||
threshold: 0.01,
|
||||
unit: "rate",
|
||||
operator: "<",
|
||||
},
|
||||
{
|
||||
name: "SOS p95 < 200ms",
|
||||
metric: "walkguide_sos_latency_ms",
|
||||
stat: "p95",
|
||||
threshold: 200,
|
||||
unit: "ms",
|
||||
operator: "<",
|
||||
},
|
||||
{
|
||||
name: "Location p95 < 300ms",
|
||||
metric: "walkguide_location_latency_ms",
|
||||
stat: "p95",
|
||||
threshold: 300,
|
||||
unit: "ms",
|
||||
operator: "<",
|
||||
},
|
||||
{
|
||||
name: "Obstacle p95 < 400ms",
|
||||
metric: "walkguide_obstacle_latency_ms",
|
||||
stat: "p95",
|
||||
threshold: 400,
|
||||
unit: "ms",
|
||||
operator: "<",
|
||||
},
|
||||
{
|
||||
name: "Auth p95 < 800ms",
|
||||
metric: "walkguide_auth_latency_ms",
|
||||
stat: "p95",
|
||||
threshold: 800,
|
||||
unit: "ms",
|
||||
operator: "<",
|
||||
},
|
||||
{
|
||||
name: "Timeline p95 < 1000ms",
|
||||
metric: "walkguide_timeline_latency_ms",
|
||||
stat: "p95",
|
||||
threshold: 1000,
|
||||
unit: "ms",
|
||||
operator: "<",
|
||||
},
|
||||
{
|
||||
name: "Notification p95 < 500ms",
|
||||
metric: "walkguide_notif_latency_ms",
|
||||
stat: "p95",
|
||||
threshold: 500,
|
||||
unit: "ms",
|
||||
operator: "<",
|
||||
},
|
||||
];
|
||||
|
||||
for (const check of thresholdChecks) {
|
||||
const m = report.metrics[check.metric];
|
||||
if (!m) {
|
||||
report.thresholdResults.push({ ...check, actual: "N/A", result: "SKIP" });
|
||||
continue;
|
||||
}
|
||||
const actual = parseFloat(m[check.stat]);
|
||||
const pass =
|
||||
check.operator === "<"
|
||||
? actual < check.threshold
|
||||
: actual > check.threshold;
|
||||
report.thresholdResults.push({
|
||||
name: check.name,
|
||||
metric: check.metric,
|
||||
threshold: check.threshold + " " + check.unit,
|
||||
actual: actual.toFixed(2) + " " + check.unit,
|
||||
result: pass ? "✅ PASS" : "❌ FAIL",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Write output ──────────────────────────────────────────────────────────────
|
||||
const outDir = path.dirname(outputFile);
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(outputFile, JSON.stringify(report, null, 2));
|
||||
console.log(`\n✅ Report written to: ${outputFile}`);
|
||||
|
||||
// ── Print summary to stdout ───────────────────────────────────────────────────
|
||||
console.log("\n═══════════════════════════════════════════════════════");
|
||||
console.log(" WalkGuide k6 — Parsed Result Summary");
|
||||
console.log("═══════════════════════════════════════════════════════");
|
||||
console.log(` Throughput: ${report.keyMetrics.throughput.value || "N/A"}`);
|
||||
console.log(` p95 Latency: ${report.keyMetrics.p95Latency.value || "N/A"}`);
|
||||
console.log(
|
||||
` Error Rate: ${report.keyMetrics.errorRate.value || "N/A"} [${report.keyMetrics.errorRate.passFail || "N/A"}]`,
|
||||
);
|
||||
console.log(` DB Est. Time: ${report.keyMetrics.dbQueryTime.value || "N/A"}`);
|
||||
console.log(` JVM Heap: ${report.keyMetrics.jvmHeap.value || "N/A"}`);
|
||||
console.log("\n Threshold Results:");
|
||||
for (const t of report.thresholdResults) {
|
||||
console.log(` ${t.result} ${t.name} (actual: ${t.actual})`);
|
||||
}
|
||||
console.log("═══════════════════════════════════════════════════════\n");
|
||||
Reference in New Issue
Block a user